← 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
A lib/slug-aliases.cjsM scripts/build-fleet.jsM scripts/scheduled-run.mjs
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 →