[object Object]

← back to Sample Followup Sweep

lib/slug-aliases.cjs: one source of truth for slug routing — the two copies had already diverged (TK-11255)

8e1807bb908faa189f92ed1f177878b9eb565f90 · 2026-09-12 08:13:05 -0700 · Steve Abrams

The alias table lived in TWO hand-maintained places: scripts/build-fleet.js
(grouping) and scripts/scheduled-run.mjs (the vid->contact bridge). They had
ALREADY drifted apart — only the bridge knew that bare vid '1838' is the same
vendor as '1838 Wallcoverings'. Two copies of one routing table is precisely the
failure mode that produced SAND->'Sandberg' and sent Sanderson memo chases to
Gimmersta, an unrelated company, earlier in this ticket. Flagged it as a smell in
6771f15; the drift was already there when I looked.

Now exactly one definition exists in the repo (verified by grep), exporting
SLUG_ALIASES, normVid and slugForVid.

Two real bugs fixed by consolidating rather than by intent:
  - build-fleet.js never stripped CR/whitespace from vid either, so a record with
    vid "kra\r" grouped under its own bogus slug. It now shares normVid() with
    scheduled-run, which got that fix in e343b5f.
  - build-fleet.js lacked the '1838' alias, so the bare vid grouped separately
    from '1838 Wallcoverings'. Both now converge on '1838-wallcoverings'.

Re-tested after the change, not assumed — including the negative test, because a
consolidation that quietly disarms the guard is the worst outcome here:
  T1 distinct vids          -> writes
  T2 ANNA FRENCH with items -> merges into thib, no stray anna-french slug
  T3 'FOO BAR' + 'FOO-BAR'  -> exit 1, fleet.json NOT written (guard still reddens)
  ESM -> CJS import proven at RUNTIME, not just by node --check, since
  scheduled-run.mjs is ESM and this module is CJS.
Live re-verification through the REAL shared module (not a reimplementation of
its logic): 27 of 28 live vendors routable, 129 overdue records, only FUT
unresolved — unchanged, so consolidation regressed nothing.

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

Files touched

Diff

commit 8e1807bb908faa189f92ed1f177878b9eb565f90
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Sep 12 08:13:05 2026 -0700

    lib/slug-aliases.cjs: one source of truth for slug routing — the two copies had already diverged (TK-11255)
    
    The alias table lived in TWO hand-maintained places: scripts/build-fleet.js
    (grouping) and scripts/scheduled-run.mjs (the vid->contact bridge). They had
    ALREADY drifted apart — only the bridge knew that bare vid '1838' is the same
    vendor as '1838 Wallcoverings'. Two copies of one routing table is precisely the
    failure mode that produced SAND->'Sandberg' and sent Sanderson memo chases to
    Gimmersta, an unrelated company, earlier in this ticket. Flagged it as a smell in
    6771f15; the drift was already there when I looked.
    
    Now exactly one definition exists in the repo (verified by grep), exporting
    SLUG_ALIASES, normVid and slugForVid.
    
    Two real bugs fixed by consolidating rather than by intent:
      - build-fleet.js never stripped CR/whitespace from vid either, so a record with
        vid "kra\r" grouped under its own bogus slug. It now shares normVid() with
        scheduled-run, which got that fix in e343b5f.
      - build-fleet.js lacked the '1838' alias, so the bare vid grouped separately
        from '1838 Wallcoverings'. Both now converge on '1838-wallcoverings'.
    
    Re-tested after the change, not assumed — including the negative test, because a
    consolidation that quietly disarms the guard is the worst outcome here:
      T1 distinct vids          -> writes
      T2 ANNA FRENCH with items -> merges into thib, no stray anna-french slug
      T3 'FOO BAR' + 'FOO-BAR'  -> exit 1, fleet.json NOT written (guard still reddens)
      ESM -> CJS import proven at RUNTIME, not just by node --check, since
      scheduled-run.mjs is ESM and this module is CJS.
    Live re-verification through the REAL shared module (not a reimplementation of
    its logic): 27 of 28 live vendors routable, 129 overdue records, only FUT
    unresolved — unchanged, so consolidation regressed nothing.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 lib/slug-aliases.cjs      | 36 ++++++++++++++++++++++++++++++++++++
 scripts/build-fleet.js    | 29 ++++++++---------------------
 scripts/scheduled-run.mjs |  7 +++----
 3 files changed, 47 insertions(+), 25 deletions(-)

diff --git a/lib/slug-aliases.cjs b/lib/slug-aliases.cjs
new file mode 100644
index 0000000..ba2ac73
--- /dev/null
+++ b/lib/slug-aliases.cjs
@@ -0,0 +1,36 @@
+'use strict';
+// SINGLE SOURCE OF TRUTH for vendor slug derivation + deliberate routing aliases.
+//
+// Why this file exists (TK-11255): this table lived in TWO places —
+// scripts/build-fleet.js (grouping) and scripts/scheduled-run.mjs (the vid->contact
+// bridge) — and had ALREADY diverged: only the bridge knew that bare vid '1838' is
+// the same vendor as '1838 Wallcoverings'. Two hand-maintained copies of one routing
+// table is exactly the failure mode that produced SAND->'Sandberg', which sent
+// Sanderson memo chases to Gimmersta, an unrelated company. One copy, or it drifts.
+//
+// A slug is normally DERIVED from the vid. The entries below intentionally route a
+// vid to ANOTHER vendor's slug because that vendor's desk fulfils their samples.
+// Adding a line is a deliberate routing decision; build-fleet.js FAILS THE BUILD on
+// any undeclared duplicate slug.
+const SLUG_ALIASES = {
+  'ANNA FRENCH': 'thib',                // Anna French is distributed by Thibaut
+  'ANNA': 'thib',                       // same vendor, short vid variant
+  'ARTE': 'arte-international',         // Arte @ Egg & Dart
+  'AS CREATION': 'san',                 // Sancar (AS Creation)
+  'BM': 'green',                        // Greenland / bmwallpaper
+  '1838': '1838-wallcoverings',         // bare vid + '1838 Wallcoverings' = one vendor (Scott Myer)
+};
+
+// FileMaker vid values carry stray CR/whitespace — a Kravet record's vid is literally
+// "kra\r". Normalise before ANY keying or the lookup silently misses and that vendor's
+// overdue sample is never chased.
+function normVid(vid) {
+  return String(vid == null ? '' : vid).replace(/[\r\n]/g, '').trim().toUpperCase();
+}
+
+function slugForVid(vid) {
+  const v = normVid(vid);
+  return SLUG_ALIASES[v] || v.toLowerCase().replace(/[^a-z0-9]+/g, '-');
+}
+
+module.exports = { SLUG_ALIASES, normVid, slugForVid };
diff --git a/scripts/build-fleet.js b/scripts/build-fleet.js
index 75bb751..b884785 100644
--- a/scripts/build-fleet.js
+++ b/scripts/build-fleet.js
@@ -3,6 +3,8 @@
 // grouped by vendor (vid), enforcing the 10–60 day window.
 const fs = require('fs');
 const path = require('path');
+// TK-11255: one shared routing table — see lib/slug-aliases.cjs for why.
+const { SLUG_ALIASES, normVid, slugForVid } = require('../lib/slug-aliases.cjs');
 
 const src = process.argv[2];
 if (!src) { console.error('usage: node scripts/build-fleet.js <fm-result.txt>'); process.exit(1); }
@@ -21,24 +23,9 @@ const NAME = {
   'ARTE INTERNATIONAL': 'Arte International', SAND: 'Sanderson', SANDB: 'Sandberg',
 };
 
-// ── Deliberate slug ALIASES — NOT a bug (TK-11255) ──────────────────────────
-// slug is derived from vid, so distinct vids normally get distinct slugs. The vids
-// below intentionally SHARE another vendor's slug because that vendor's desk
-// fulfils their samples (Thibaut distributes Anna French, so an ANNA FRENCH memo
-// is chased at contacts['thib']). They ship as items:0 stub rows so vidContactMap
-// resolves.
-//
-// These used to be HAND-EDITED into data/fleet.json, so a regeneration would have
-// silently erased them and broken contact routing. Declaring them here makes them
-// survive a rebuild AND makes them reviewable. Adding a line is a deliberate
-// routing decision; any UNDECLARED duplicate slug now fails the build (below).
-const SLUG_ALIASES = {
-  'ANNA FRENCH': 'thib',            // Anna French distributed by Thibaut
-  'ANNA': 'thib',                   // same vendor, short vid variant
-  'ARTE': 'arte-international',     // Arte @ Egg & Dart
-  'AS CREATION': 'san',             // Sancar (AS Creation)
-  'BM': 'green',                    // Greenland / bmwallpaper
-};
+// Deliberate slug ALIASES and vid normalisation now live in lib/slug-aliases.cjs
+// (single source of truth — this table used to be duplicated here and in
+// scripts/scheduled-run.mjs, and the two copies had already diverged).
 
 const g = {};
 let skippedNoVid = 0, skippedWindow = 0;
@@ -48,15 +35,15 @@ for (const r of j.records) {
   if (!rawvid) { skippedNoVid++; continue; }
   // Merge duplicate vids for the same vendor (e.g. SCHUMACHER + SCH → one Schumacher row).
   const VID_ALIAS = { SCHUMACHER: 'SCH' };
-  const vid = VID_ALIAS[rawvid.toUpperCase()] || rawvid.toUpperCase();
+  const vid = VID_ALIAS[normVid(rawvid)] || normVid(rawvid);
   const dt = d['today for client'];
   const age = dt ? Math.floor((today - new Date(dt)) / 86400000) : null;
   if (age === null || age <= 10 || age > 60) { skippedWindow++; continue; }
-  const natural = vid.toLowerCase().replace(/[^a-z0-9]+/g, '-');
+  const natural = vid.toLowerCase().replace(/[^a-z0-9]+/g, '-');   // pre-alias form, used by the guard below
   // A declared alias routes this vid's memos to ANOTHER vendor's desk (Anna French
   // -> Thibaut). Applied HERE, not after grouping, so the routing is the same
   // whether or not the aliased vid happens to have items in this window.
-  const slug = SLUG_ALIASES[vid] || natural;
+  const slug = slugForVid(vid);
   // Two DISTINCT vids landing on one slug: the old code merged them silently via
   // `g[slug] = g[slug] || …`, attributing one vendor's outstanding samples to the
   // other. Intentional only when declared above; otherwise refuse to build.
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 379620a..68a1974 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -17,6 +17,8 @@
 //   node scripts/scheduled-run.mjs --send     # actually send + stamp
 
 import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
+import slugAliases from '../lib/slug-aliases.cjs';   // TK-11255: single source of truth
+const { slugForVid } = slugAliases;
 import { fileURLToPath } from 'node:url';
 import { dirname, join } from 'node:path';
 import { homedir } from 'node:os';
@@ -157,10 +159,7 @@ function vidContactMap(liveVids = []) {
   // 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',
-    // bare vid '1838' and '1838 Wallcoverings' are the same vendor (Scott Myer).
-    '1838': '1838-wallcoverings' };
-  const slugForVid = (vid) => SLUG_ALIASES[vid] || String(vid).toLowerCase().replace(/[^a-z0-9]+/g, '-');
+  // SLUG_ALIASES + slugForVid come from lib/slug-aliases.cjs (one copy, see header there).
   const put = (key, slug) => {
     if (!key || map[key]) return;
     const c = contacts[slug]; if (!c) return;

← 5c0d68e contacts.json: route VAH + BRU + bare-1838 — 27 of 28 live v  ·  back to Sample Followup Sweep  ·  auto-data-snapshot: 2026-09-12T08:20:59 (5 data files) — dat e97b899 →