← back to Sample Followup Sweep
build-fleet: declare slug aliases + fail the build on an undeclared collision (TK-11255)
e0317ebb75e8e473bb8a507686f5f2f65d0310e3 · 2026-09-12 07:15:43 -0700 · Steve Abrams
DTD verdict B, narrowly scoped. Peer commit 706e430 already fixed the SAND name
constant, so this is the remaining half and deliberately does NOT attempt the
rearchitecture the panel's D voters proposed.
WHAT THE PANEL GOT WRONG, and why this is small: 3 of 5 voted D ('identity does
not belong in a derived artifact') on a premise I supplied incorrectly. The
duplicate slugs are NOT a defect -- slug derives from vid, so THIB/ANNA FRENCH/
ANNA yield three DISTINCT slugs and the generator provably cannot collide them.
Every duplicate row has items:0 and commit ae6499b says 'fleet stubs so
vidContactMap resolves': they are deliberate distributor-alias stubs (Thibaut
distributes Anna French). Deduping them would have BROKEN contact routing.
Three changes:
1. SLUG_ALIASES -- the hand-edited stubs are now an explicit, commented,
version-controlled whitelist. They previously lived only as hand edits in
data/fleet.json, so any regeneration would have silently erased them.
2. The alias is applied when DERIVING the slug, not bolted on afterwards. A
negative test caught that ANNA FRENCH only routed to 'thib' while it had NO
items; give it real samples and it got its own 'anna-french' slug with no
contact behind it. Now routing is identical either way, and the aliased vid is
recorded in aliasVids so stamping/reply-matching don't lose it.
3. A build-time guard at GROUPING time, not on the output rows. The first
version of this guard PASSED its own injected-fault test for the wrong reason:
the generator's own `g[slug] = g[slug] || {...}` had already merged the
colliding vendors into one
row, so by the time the output was inspected the collision was invisible. That
is a worse bug than the one I set out to guard -- two distinct vids that
normalize to one slug silently attribute one vendor's outstanding samples to
the other. It now refuses to write fleet.json at all.
Tested (positive, alias, and injected fault -- a positive-only test would have
shipped the broken first guard):
T1 distinct vids -> writes, no false failure
T2 ANNA FRENCH WITH items -> merges into thib, aliasVids recorded, no stray
anna-french slug
T3 'FOO BAR' + 'FOO-BAR' -> exit 1, fleet.json NOT written
Run in an isolated tree so the real data/fleet.json was never touched.
Consumer hardening: server.js gains vendorBySlug() and scripts/fmpro.mjs an
inline equivalent, replacing 4 bare .find(x => x.slug === s) calls that depended
on stubs sorting last. Verified behaviour-identical on today's data -- this
removes fragility, it does not fix a live bug.
NOT done on purpose: no runtime join to canonical FileMaker. It is hand-typed,
17% of its sample-email values are not addresses, it became API-readable only
yesterday, and the mailer runs entirely off local files -- that trades a rare
silent-wrong-name for an intermittent cannot-send with nobody on call.
fleet.json is NOT regenerated here; regenerating is now safe but is a separate,
reviewable step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M scripts/build-fleet.jsM scripts/fmpro.mjsM server.js
Diff
commit e0317ebb75e8e473bb8a507686f5f2f65d0310e3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 12 07:15:43 2026 -0700
build-fleet: declare slug aliases + fail the build on an undeclared collision (TK-11255)
DTD verdict B, narrowly scoped. Peer commit 706e430 already fixed the SAND name
constant, so this is the remaining half and deliberately does NOT attempt the
rearchitecture the panel's D voters proposed.
WHAT THE PANEL GOT WRONG, and why this is small: 3 of 5 voted D ('identity does
not belong in a derived artifact') on a premise I supplied incorrectly. The
duplicate slugs are NOT a defect -- slug derives from vid, so THIB/ANNA FRENCH/
ANNA yield three DISTINCT slugs and the generator provably cannot collide them.
Every duplicate row has items:0 and commit ae6499b says 'fleet stubs so
vidContactMap resolves': they are deliberate distributor-alias stubs (Thibaut
distributes Anna French). Deduping them would have BROKEN contact routing.
Three changes:
1. SLUG_ALIASES -- the hand-edited stubs are now an explicit, commented,
version-controlled whitelist. They previously lived only as hand edits in
data/fleet.json, so any regeneration would have silently erased them.
2. The alias is applied when DERIVING the slug, not bolted on afterwards. A
negative test caught that ANNA FRENCH only routed to 'thib' while it had NO
items; give it real samples and it got its own 'anna-french' slug with no
contact behind it. Now routing is identical either way, and the aliased vid is
recorded in aliasVids so stamping/reply-matching don't lose it.
3. A build-time guard at GROUPING time, not on the output rows. The first
version of this guard PASSED its own injected-fault test for the wrong reason:
the generator's own `g[slug] = g[slug] || {...}` had already merged the
colliding vendors into one
row, so by the time the output was inspected the collision was invisible. That
is a worse bug than the one I set out to guard -- two distinct vids that
normalize to one slug silently attribute one vendor's outstanding samples to
the other. It now refuses to write fleet.json at all.
Tested (positive, alias, and injected fault -- a positive-only test would have
shipped the broken first guard):
T1 distinct vids -> writes, no false failure
T2 ANNA FRENCH WITH items -> merges into thib, aliasVids recorded, no stray
anna-french slug
T3 'FOO BAR' + 'FOO-BAR' -> exit 1, fleet.json NOT written
Run in an isolated tree so the real data/fleet.json was never touched.
Consumer hardening: server.js gains vendorBySlug() and scripts/fmpro.mjs an
inline equivalent, replacing 4 bare .find(x => x.slug === s) calls that depended
on stubs sorting last. Verified behaviour-identical on today's data -- this
removes fragility, it does not fix a live bug.
NOT done on purpose: no runtime join to canonical FileMaker. It is hand-typed,
17% of its sample-email values are not addresses, it became API-readable only
yesterday, and the mailer runs entirely off local files -- that trades a rare
silent-wrong-name for an intermittent cannot-send with nobody on call.
fleet.json is NOT regenerated here; regenerating is now safe but is a separate,
reviewable step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
scripts/build-fleet.js | 68 +++++++++++++++++++++++++++++++++++++++++++++++---
scripts/fmpro.mjs | 6 ++++-
server.js | 14 ++++++++---
3 files changed, 81 insertions(+), 7 deletions(-)
diff --git a/scripts/build-fleet.js b/scripts/build-fleet.js
index f118b87..75bb751 100644
--- a/scripts/build-fleet.js
+++ b/scripts/build-fleet.js
@@ -21,6 +21,25 @@ 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
+};
+
const g = {};
let skippedNoVid = 0, skippedWindow = 0;
for (const r of j.records) {
@@ -33,8 +52,26 @@ for (const r of j.records) {
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 slug = vid.toLowerCase().replace(/[^a-z0-9]+/g, '-');
+ const natural = vid.toLowerCase().replace(/[^a-z0-9]+/g, '-');
+ // 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;
+ // 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.
+ if (g[slug] && String(g[slug].vid).toUpperCase() !== vid && SLUG_ALIASES[vid] !== slug) {
+ console.error(`FATAL: vids '${g[slug].vid}' and '${vid}' both derive slug '${slug}'.`);
+ console.error(`Their samples would be silently merged into one vendor. If that routing is`);
+ console.error(`intentional add '${vid}': '${slug}', to SLUG_ALIASES. Refusing to write fleet.json.`);
+ process.exit(1);
+ }
g[slug] = g[slug] || { slug, vid, name: NAME[vid] || `(vid ${rawvid})`, items: [] };
+ // Keep the aliased vid visible so stamping/reply-matching don't lose it.
+ if (String(g[slug].vid).toUpperCase() !== vid) {
+ g[slug].aliasVids = g[slug].aliasVids || [];
+ if (!g[slug].aliasVids.includes(vid)) g[slug].aliasVids.push(vid);
+ }
g[slug].items.push({
mfr: (d['Mfr Pattern'] || '').replace(/\r/g, '').replace(/\($/, '').trim(),
dw: (d['combo sku'] || '').trim(),
@@ -58,12 +95,37 @@ const vendors = Object.values(g).map(v => {
return { ...v, items, count: items.length, reReqCount: items.filter(x => x.reRequested).length };
}).sort((a, b) => b.count - a.count);
+// Emit the declared alias stubs unless that vid already has a real group of its
+// own, so a rebuild cannot drop routing that a hand-edit used to supply.
+const aliasStubs = Object.entries(SLUG_ALIASES)
+ .filter(([vid]) => !vendors.some((v) => String(v.vid).toUpperCase() === vid.toUpperCase()))
+ .map(([vid, slug]) => ({ slug, vid, name: NAME[vid] || `(vid ${vid})`, items: [], count: 0, reReqCount: 0 }));
+const allVendors = [...vendors, ...aliasStubs];
+
+// Build-time guard: a slug may repeat ONLY through a declared alias. An undeclared
+// collision means new source data or a bad edit, and must NOT ship -- a consumer
+// doing a last-wins {slug: vid} cast silently drops the extra rows (that is exactly
+// how a phantom "drift" finding was manufactured during the TK-11255 audit).
+const bySlug = {};
+for (const v of allVendors) (bySlug[v.slug] = bySlug[v.slug] || []).push(String(v.vid).toUpperCase());
+for (const [slug, vids] of Object.entries(bySlug)) {
+ if (vids.length < 2) continue;
+ for (const vd of vids) {
+ const natural = vd.toLowerCase().replace(/[^a-z0-9]+/g, '-') === slug;
+ if (!natural && SLUG_ALIASES[vd] !== slug) {
+ console.error(`FATAL: undeclared duplicate slug '${slug}' for vid '${vd}' (slug shared by: ${vids.join(', ')}).`);
+ console.error(`If that routing is intentional add '${vd}': '${slug}', to SLUG_ALIASES. Refusing to write fleet.json.`);
+ process.exit(1);
+ }
+ }
+}
+
const out = {
generated: '2026-08-14',
window: '10–60 days (>60 = dead lead, excluded)',
- totalItems: vendors.reduce((s, v) => s + v.count, 0),
+ totalItems: allVendors.reduce((s, v) => s + v.count, 0),
skippedNoVid, skippedWindow,
- vendors,
+ vendors: allVendors,
};
fs.writeFileSync(path.join(__dirname, '..', 'data', 'fleet.json'), JSON.stringify(out, null, 2));
console.log(`vendors=${vendors.length} items=${out.totalItems} (skipped: no-vid ${skippedNoVid}, out-of-window ${skippedWindow})`);
diff --git a/scripts/fmpro.mjs b/scripts/fmpro.mjs
index 16aff9d..016166a 100644
--- a/scripts/fmpro.mjs
+++ b/scripts/fmpro.mjs
@@ -257,7 +257,11 @@ if (cmd === 'plan') {
// first-chase behaviour, so nothing that calls `stamp` without --pass changes.
const pass = String(arg('pass') || '1') === '2' ? 2 : 1;
if (!slug) { console.error('need --slug'); process.exit(1); }
- const fleetV = JSON.parse(readFileSync(join(ROOT, 'data', 'fleet.json'), 'utf8')).vendors.find((x) => x.slug === slug);
+ // TK-11255: slug is legitimately non-unique (declared aliases ship as items:0
+ // stubs on another vendor's slug); prefer the row that actually has items
+ // instead of depending on array order.
+ const _fv = JSON.parse(readFileSync(join(ROOT, 'data', 'fleet.json'), 'utf8')).vendors.filter((x) => x.slug === slug);
+ const fleetV = _fv.find((x) => x.items && x.items.length) || _fv[0];
if (!fleetV) { console.error(`unknown --slug ${slug}`); process.exit(1); }
const r = await stampVendor({ slug: fleetV.slug, vid: fleetV.vid, name: fleetV.name, items: fleetV.items, date }, pass);
if (r.count) recordPosted(fleetV.vid, r);
diff --git a/server.js b/server.js
index 3f9ad51..59be810 100644
--- a/server.js
+++ b/server.js
@@ -48,6 +48,14 @@ function stampFmpro(slug, pass = 1) {
} catch (e) { /* never blocks the send */ }
}
const staged = () => readJSON(p('out', 'all-drafts.json'), []); // the EXACT letters staged/sent
+// TK-11255: a slug is legitimately NON-UNIQUE — declared aliases (Anna French ->
+// Thibaut) ship as items:0 stub rows sharing another vendor's slug. A bare
+// .find(x => x.slug === s) therefore depends on array order to land on the real
+// row. It happens to work today only because stubs sort last; make it explicit.
+function vendorBySlug(vendors, slug) {
+ const hits = vendors.filter((v) => v.slug === slug);
+ return hits.find((v) => v.items && v.items.length) || hits[0] || undefined;
+}
function stagedFor(slug) { return (staged() || []).find(d => (d.slugs || []).includes(slug)); }
function vendorObj(v, c) {
@@ -125,7 +133,7 @@ const server = http.createServer(async (req, res) => {
// Serve the EXACT letter that was staged/sent (consolidated + cleaned), so preview == what the vendor got.
const st = stagedFor(b.slug);
if (st) return send(res, 200, { to: st.to, subject: st.subject, html: st.body, count: st.items, exact: true });
- const v = fleet().vendors.find(x => x.slug === b.slug);
+ const v = vendorBySlug(fleet().vendors, b.slug);
if (!v) return send(res, 404, { error: 'no vendor' });
const d = draftFor(v, contacts());
return send(res, 200, { to: d.to, subject: d.subject, html: d.html, count: v.items.length, exact: false });
@@ -141,7 +149,7 @@ const server = http.createServer(async (req, res) => {
const st = stagedFor(b.slug);
let payload;
if (st) payload = { account: 'info', to: st.to, subject: st.subject, body: st.body };
- else { const v = fleet().vendors.find(x => x.slug === b.slug); if (!v) return send(res, 404, { error: 'no vendor' }); const d = draftFor(v, c); payload = { account: 'info', to: d.to, subject: d.subject, body: d.html }; }
+ else { const v = vendorBySlug(fleet().vendors, b.slug); if (!v) return send(res, 404, { error: 'no vendor' }); const d = draftFor(v, c); payload = { account: 'info', to: d.to, subject: d.subject, body: d.html }; }
// Was this vendor already emailed BEFORE this send? Computed from sent.json (the record of
// fact), not from b.force — someone can force a genuine first send, and that must stay pass 1.
const _addrs = String(payload.to).split(',').map(a => a.trim().toLowerCase()).filter(a => a.includes('@'));
@@ -175,7 +183,7 @@ const server = http.createServer(async (req, res) => {
if (u.pathname === '/api/queue' && req.method === 'POST') {
const b = await body(req); const c = contacts(); const vs = fleet().vendors;
- const picked = (b.slugs || []).map(s => vs.find(v => v.slug === s)).filter(Boolean);
+ const picked = (b.slugs || []).map(s => vendorBySlug(vs, s)).filter(Boolean);
const queue = picked.map(v => {
const cc = c[v.slug] || {}; const d = draftFor(v, c);
const missing = []; if (!cc.sample_email) missing.push('sample_email'); if (!cc.account_number) missing.push('account_number');
← 47b5b8a auto-data-snapshot: 2026-09-11T15:15:48 (2 data files) — dat
·
back to Sample Followup Sweep
·
chore: v1.1.0 -> v1.1.1 (TK-11255 vendor-desk routing fix, s eafdc77 →