[object Object]

← back to Dw Validator Debug TK11314

fix(importer): fail-loud on missing per-color mfr sku instead of fabricating a counter

9d08204f6684ffff63651629518f2f4b78ba61db · 2026-09-01 08:02:27 -0700 · Steve Abrams

Root cause of the go-live-gate window growth (TK-11063): importer/scraper sites did
`sku: color.sku || ${pattern_number}-${color_number}` (and `VENDOR-${i+1}`),
minting a fabricated sequential code when the real per-color mfr code was not captured
— exactly how Carnegie Grain 6300 became 63001..630026. Adds shared fail-loud helper
lib/resolve-mfr-sku.ts: returns the real sku or HOLDS the row (never fabricates);
by-colour lines (Novasuede) exempt. Wired into fix-all-vendors-structure.ts,
fetch-vendor-products.ts (in-browser emits '' + Node-side holds), and the DW-Agents
Arte importer. Rule dw-dwsku-keep-old-never-mint. TK-11063.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 9d08204f6684ffff63651629518f2f4b78ba61db
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 08:02:27 2026 -0700

    fix(importer): fail-loud on missing per-color mfr sku instead of fabricating a counter
    
    Root cause of the go-live-gate window growth (TK-11063): importer/scraper sites did
    `sku: color.sku || ${pattern_number}-${color_number}` (and `VENDOR-${i+1}`),
    minting a fabricated sequential code when the real per-color mfr code was not captured
    — exactly how Carnegie Grain 6300 became 63001..630026. Adds shared fail-loud helper
    lib/resolve-mfr-sku.ts: returns the real sku or HOLDS the row (never fabricates);
    by-colour lines (Novasuede) exempt. Wired into fix-all-vendors-structure.ts,
    fetch-vendor-products.ts (in-browser emits '' + Node-side holds), and the DW-Agents
    Arte importer. Rule dw-dwsku-keep-old-never-mint. TK-11063.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 DW-Agents/scripts/import-products.ts               | 12 ++++-
 .../__tests__/resolve-mfr-sku.test.ts              | 33 +++++++++++++
 .../ImportNewSkufromURL/fetch-vendor-products.ts   | 20 ++++++--
 .../fix-all-vendors-structure.ts                   | 19 ++++++--
 .../ImportNewSkufromURL/lib/resolve-mfr-sku.ts     | 54 ++++++++++++++++++++++
 5 files changed, 131 insertions(+), 7 deletions(-)

diff --git a/DW-Agents/scripts/import-products.ts b/DW-Agents/scripts/import-products.ts
index 75f1a1d3..5870b949 100755
--- a/DW-Agents/scripts/import-products.ts
+++ b/DW-Agents/scripts/import-products.ts
@@ -76,11 +76,21 @@ async function importArteProducts(): Promise<ImportStats> {
             if (pattern.colors) {
               for (const color of pattern.colors) {
                 stats.total++;
+                // FAIL-LOUD (TK-11063): never fabricate `${pattern_number}-${color_number}`
+                // when the REAL per-color sku was not captured — that is exactly the
+                // Carnegie 6300→63001.. garbage-code class (rule dw-dwsku-keep-old-never-mint).
+                // HOLD the colorway (skip + count) so an incomplete scrape is visible.
+                const realSku = typeof color.sku === 'string' ? color.sku.trim() : '';
+                if (!realSku) {
+                  stats.failed++;
+                  stats.errors.push(`Arte HOLD ${pattern.name} - ${color.name}: missing-real-mfr-sku (no fabricated code minted)`);
+                  continue;
+                }
                 try {
                   await prisma.products.create({
                     data: {
                       vendor_id: 'arte-international',
-                      sku: color.sku || `${pattern.pattern_number}-${color.color_number}`,
+                      sku: realSku,
                       name: `${pattern.name} - ${color.name}`,
                       product_url: color.url || '',
                       pattern_name: pattern.name,
diff --git a/DW-Programming/ImportNewSkufromURL/__tests__/resolve-mfr-sku.test.ts b/DW-Programming/ImportNewSkufromURL/__tests__/resolve-mfr-sku.test.ts
new file mode 100644
index 00000000..0c391ef8
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/__tests__/resolve-mfr-sku.test.ts
@@ -0,0 +1,33 @@
+import { describe, it, expect } from '@jest/globals';
+import { resolveMfrSku } from '../lib/resolve-mfr-sku';
+
+describe('resolveMfrSku — fail-loud (TK-11063)', () => {
+  it('returns the real sku when present', () => {
+    expect(resolveMfrSku('24121-windows', 'carnegie')).toEqual({ ok: true, sku: '24121-windows' });
+  });
+
+  it('HOLDS (never fabricates) when the real sku is missing on a code-required line', () => {
+    for (const missing of ['', '   ', undefined, null]) {
+      const r = resolveMfrSku(missing as any, 'carnegie');
+      expect(r.ok).toBe(false);
+      expect(r.sku).toBeNull();
+      expect(r.reason).toBe('missing-real-mfr-sku');
+    }
+  });
+
+  it('treats literal "unknown" as missing', () => {
+    const r = resolveMfrSku('Unknown', 'arte-international');
+    expect(r.ok).toBe(false);
+    expect(r.sku).toBeNull();
+  });
+
+  it('exempts by-colour lines (Novasuede) — missing code is allowed, empty sku ok', () => {
+    const r = resolveMfrSku('', 'novasuede');
+    expect(r.ok).toBe(true);
+    expect(r.sku).toBe('');
+  });
+
+  it('trims whitespace on a real code', () => {
+    expect(resolveMfrSku('  T10958  ', 'thibaut')).toEqual({ ok: true, sku: 'T10958' });
+  });
+});
diff --git a/DW-Programming/ImportNewSkufromURL/fetch-vendor-products.ts b/DW-Programming/ImportNewSkufromURL/fetch-vendor-products.ts
index 880a266d..455ff2c6 100644
--- a/DW-Programming/ImportNewSkufromURL/fetch-vendor-products.ts
+++ b/DW-Programming/ImportNewSkufromURL/fetch-vendor-products.ts
@@ -6,6 +6,7 @@ import { chromium, Browser } from 'playwright';
 import * as fs from 'fs';
 import sqlite3 from 'better-sqlite3';
 import * as dotenv from 'dotenv';
+import { resolveMfrSku } from './lib/resolve-mfr-sku';
 
 dotenv.config({ path: '.env.local' });
 
@@ -93,8 +94,18 @@ class VendorProductFetcher {
       await page.waitForTimeout(waitTime);
       
       // Extract products based on vendor-specific selectors
-      const products = await this.extractProducts(page, vendor.vendor_id);
-      
+      const rawProducts = await this.extractProducts(page, vendor.vendor_id);
+
+      // FAIL-LOUD (TK-11063): HOLD rows with no REAL captured mfr sku instead of the
+      // in-browser code fabricating one. resolveMfrSku returns ok:false for a missing
+      // code on a code-required line → drop + count (visible incompleteness, never a
+      // fabricated `${VENDOR}-${i+1}` code entering the pipeline).
+      const products = rawProducts.filter((p: any) => resolveMfrSku(p.sku, vendor.vendor_id).ok);
+      const heldMissingMfr = rawProducts.length - products.length;
+      if (heldMissingMfr > 0) {
+        console.log(`   ⚠️ Held ${heldMissingMfr} product(s) with no real mfr sku (not fabricated)`);
+      }
+
       result.products_found = products.length;
       result.products = products;
       
@@ -188,7 +199,10 @@ class VendorProductFetcher {
         
         results.push({
           title: title,
-          sku: sku || `${vendorId.toUpperCase()}-${i + 1}`,
+          // FAIL-LOUD (TK-11063): NEVER fabricate a `${VENDOR}-${i+1}` sequential code
+          // in-browser. Emit '' when no real sku was captured; the Node side HOLDS it via
+          // resolveMfrSku so an incomplete scrape is visible, not silently poisoned.
+          sku: sku || '',
           url: fullUrl,
           image: image,
           vendor: vendorId
diff --git a/DW-Programming/ImportNewSkufromURL/fix-all-vendors-structure.ts b/DW-Programming/ImportNewSkufromURL/fix-all-vendors-structure.ts
index 545d7592..b6039fd1 100644
--- a/DW-Programming/ImportNewSkufromURL/fix-all-vendors-structure.ts
+++ b/DW-Programming/ImportNewSkufromURL/fix-all-vendors-structure.ts
@@ -1,15 +1,17 @@
 #!/usr/bin/env npx tsx
 
 import * as fs from 'fs';
+import { resolveMfrSku } from './lib/resolve-mfr-sku';
 
 function fixAllVendorsStructure() {
   console.log('🚀 Fixing ALL vendors to follow Collection-Pattern-Colors structure...\n');
-  
+
   const resultsPath = '/root/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/public/vendor-test-results.json';
   const data = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'));
-  
+
   let fixedCount = 0;
   let alreadyCorrectCount = 0;
+  let heldMissingMfr = 0; // rows dropped because no REAL mfr code was captured (fail-loud, TK-11063)
   
   // Process each vendor
   for (const vendorId in data.vendors) {
@@ -62,11 +64,21 @@ function fixAllVendorsStructure() {
           });
         }
         
+        // FAIL-LOUD (TK-11063): resolve the REAL captured sku — NEVER fabricate a
+        // `${VENDOR}-${n}` sequential counter (the Carnegie 6300→63001.. bug class,
+        // standing rule dw-dwsku-keep-old-never-mint). A row with no real code is
+        // HELD (skipped + counted) so the scrape is visibly incomplete, not poisoned.
+        const mfr = resolveMfrSku(item.sku, vendorId);
+        if (!mfr.ok) {
+          heldMissingMfr++;
+          console.warn(`  ⚠ HOLD ${vendorId} "${item.title || patternName}" — ${mfr.reason} (no fabricated code minted)`);
+          return; // do not push a fabricated-code product
+        }
         const pattern = patternMap.get(patternName);
         pattern.products.push({
           type: 'product',
           title: item.title || `${patternName} - ${colorName}`,
-          sku: item.sku || `${vendorId.toUpperCase()}-${pattern.products.length + 1}`,
+          sku: mfr.sku,
           url: item.url || vendor.url || ''
         });
         pattern.colorCount = pattern.products.length;
@@ -97,6 +109,7 @@ function fixAllVendorsStructure() {
   console.log(`✅ COMPLETED ALL VENDOR FIXES`);
   console.log(`   Fixed: ${fixedCount} vendors`);
   console.log(`   Already correct: ${alreadyCorrectCount} vendors`);
+  console.log(`   Held (no real mfr sku, NOT fabricated): ${heldMissingMfr} product(s)`);
   console.log(`   Total vendors: ${Object.keys(data.vendors).length}`);
   console.log('=================================\n');
   
diff --git a/DW-Programming/ImportNewSkufromURL/lib/resolve-mfr-sku.ts b/DW-Programming/ImportNewSkufromURL/lib/resolve-mfr-sku.ts
new file mode 100644
index 00000000..2e35af14
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/resolve-mfr-sku.ts
@@ -0,0 +1,54 @@
+/*
+ * resolve-mfr-sku.ts — FAIL-LOUD manufacturer-SKU resolver for the import engine.
+ *
+ * Root cause of the go-live-gate FAILs (TK-11063): importer/scraper sites did
+ *     sku: color.sku || `${pattern_number}-${color_number}`   // or `VENDOR-${i+1}`
+ * — i.e. when the REAL per-color manufacturer SKU was NOT captured, they FABRICATED a
+ * sequential counter. That is exactly how Carnegie "Grain 6300" became the fake codes
+ * 63001..630026 (base pattern 6300 + ordinal color_number 1..N), which then flowed to
+ * staging and went live with a garbage mfr number. Standing rule [[dw-dwsku-keep-old-never-mint]]:
+ * never mint a new sequential code — preserve a REAL existing code or fail loud.
+ *
+ * This resolver replaces every "|| fabricate" fallback: it returns the REAL sku when
+ * present, otherwise NULL — and the caller MUST HOLD/SKIP the row (never insert), so a
+ * scrape that fails to capture real per-color codes is VISIBLY incomplete instead of
+ * silently poisoning the catalog. Per-vendor policy exempts legitimately code-less
+ * by-colour lines (e.g. Novasuede) — for those, missing is allowed (returns '' ok:true).
+ */
+
+// By-COLOUR private lines that legitimately have NO per-color manufacturer code
+// (kept in sync with the activator gate's NOMFR_EXEMPT_VENDORS + the canary).
+const NOMFR_EXEMPT_VENDORS = ['novasuede'];
+
+export interface ResolveMfrResult {
+  /** true = caller may proceed; false = caller MUST hold/skip (do not create the row). */
+  ok: boolean;
+  /** the real sku when ok && present; '' for an exempt code-less line; null on a hold. */
+  sku: string | null;
+  /** machine reason for a hold, for logging/alerting. */
+  reason?: string;
+}
+
+function isExemptVendor(vendorId?: string): boolean {
+  const v = String(vendorId || '').toLowerCase();
+  return NOMFR_EXEMPT_VENDORS.some((e) => v.includes(e));
+}
+
+/**
+ * Resolve the manufacturer SKU for a single colorway, FAIL-LOUD (never fabricate).
+ *
+ * @param realSku   the sku actually captured by the scrape (color.sku / item.sku / vSku).
+ *                  Empty/whitespace/undefined ⇒ "not captured".
+ * @param vendorId  the vendor id (for the by-colour exemption policy).
+ * @returns ok:false + reason when the real code is missing on a code-REQUIRED line —
+ *          the caller must skip + log; ok:true + sku otherwise.
+ */
+export function resolveMfrSku(realSku: unknown, vendorId?: string): ResolveMfrResult {
+  const s = typeof realSku === 'string' ? realSku.trim() : (realSku == null ? '' : String(realSku).trim());
+  if (s && !/^unknown$/i.test(s)) return { ok: true, sku: s };
+  // Missing/blank real code.
+  if (isExemptVendor(vendorId)) return { ok: true, sku: '', reason: 'by-colour-line-no-mfr-code' };
+  return { ok: false, sku: null, reason: 'missing-real-mfr-sku' };
+}
+
+export { NOMFR_EXEMPT_VENDORS, isExemptVendor };

← 4428bcdb auto-data-snapshot: 2026-09-01T07:51:05 (3 data files) — dat  ·  back to Dw Validator Debug TK11314  ·  TK-11047 verify 500 pending Carnegie specs b4aa396b →