[object Object]

← back to Designerwallcoverings

Sanderson importer: fix color-tag source + selective image dedup (TK-11070)

349d204f1b930cd75e81821cfa137491f1d9afda · 2026-09-01 10:43:12 -0700 · Steve Abrams

Two root-cause fixes to scripts/sanderson-onboard/build-payloads.mjs:

1. color: facet now sources from the REAL scraped colorway (color_name) instead
   of the AI-vision background-color guess (ai_background_color). Falls back to
   emitting no color: tag rather than a wrong one when color_name is absent.
   Also demotes ai_colors bare palette tags and strips ai_tags entries that
   exactly match an AI-palette name (Mist/Sky/Duck Egg, Ivory/Stone/Oatmeal),
   which were reading as stray-colorway pollution.

2. images() is now SELECTIVE, not blanket. gallery_images was a rolling window
   of adjacent-pattern carousel plates + CN* paint chips (verified: vendor
   JSON-LD declares a single hero == image_url; the only legit extra shots embed
   THIS product's own SAW code in the filename, 337/337 matching their own row).
   Keeps image_url hero (pos 1) + gallery images whose filename embeds this row's
   own SAW article code; drops all foreign plates. Preserves 191 products' legit
   multi-angle shots; never drops to 0 images (all 594 retain >=1).

Dry-run verified: 594 payloads, 0 zero-image, 0 missing color: facet.

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

Files touched

Diff

commit 349d204f1b930cd75e81821cfa137491f1d9afda
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 10:43:12 2026 -0700

    Sanderson importer: fix color-tag source + selective image dedup (TK-11070)
    
    Two root-cause fixes to scripts/sanderson-onboard/build-payloads.mjs:
    
    1. color: facet now sources from the REAL scraped colorway (color_name) instead
       of the AI-vision background-color guess (ai_background_color). Falls back to
       emitting no color: tag rather than a wrong one when color_name is absent.
       Also demotes ai_colors bare palette tags and strips ai_tags entries that
       exactly match an AI-palette name (Mist/Sky/Duck Egg, Ivory/Stone/Oatmeal),
       which were reading as stray-colorway pollution.
    
    2. images() is now SELECTIVE, not blanket. gallery_images was a rolling window
       of adjacent-pattern carousel plates + CN* paint chips (verified: vendor
       JSON-LD declares a single hero == image_url; the only legit extra shots embed
       THIS product's own SAW code in the filename, 337/337 matching their own row).
       Keeps image_url hero (pos 1) + gallery images whose filename embeds this row's
       own SAW article code; drops all foreign plates. Preserves 191 products' legit
       multi-angle shots; never drops to 0 images (all 594 retain >=1).
    
    Dry-run verified: 594 payloads, 0 zero-image, 0 missing color: facet.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/sanderson-onboard/build-payloads.mjs | 51 ++++++++++++++++++++++++----
 1 file changed, 45 insertions(+), 6 deletions(-)

diff --git a/scripts/sanderson-onboard/build-payloads.mjs b/scripts/sanderson-onboard/build-payloads.mjs
index e3330e2..087e92d 100644
--- a/scripts/sanderson-onboard/build-payloads.mjs
+++ b/scripts/sanderson-onboard/build-payloads.mjs
@@ -45,7 +45,7 @@ const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&':'&amp
 
 function rows() {
   const cols = `id, mfr_sku, dw_sku, pattern_name, color_name, collection, width, roll_length,
-    pattern_repeat, material, image_url, gallery_images, retail_usd, cost_usd, ai_description,
+    pattern_repeat, material, image_url, product_url, gallery_images, retail_usd, cost_usd, ai_description,
     to_json(ai_colors) ai_colors, ai_background_color, to_json(ai_styles) ai_styles,
     to_json(ai_patterns) ai_patterns, to_json(ai_tags) ai_tags, to_json(styles) styles,
     settlement_verdict`;
@@ -69,12 +69,28 @@ function tags(r) {
   const t = new Set(['Wallcovering', 'Wallcoverings', 'Sanderson', 'Sanderson Wallcovering']);
   if (!bad(r.collection)) t.add(titleCase(r.collection));
   if (!bad(r.material)) t.add(titleCase(r.material));
-  if (!bad(r.ai_background_color)) t.add('color:' + titleCase(r.ai_background_color));
-  for (const c of arr(r.ai_colors)) { const n = c && (c.name || c.color); if (!bad(n)) t.add(titleCase(n)); }
+  // color: facet MUST come from the REAL scraped colorway (color_name), NOT AI vision guess (TK-11070).
+  // Falls back gracefully to nothing (never emits a wrong color:) if the real colorway is missing.
+  if (!bad(r.color_name)) t.add('color:' + titleCase(r.color_name));
+  // NOTE (TK-11070): ai_colors palette names below (Alabaster/Sky/Duck Egg …) are DEMOTED — they were
+  // producing stray-colorway pollution tags. They are no longer added as bare tags; the color: facet is
+  // now the single authoritative colorway. Palette hues remain available via custom.color_details metafield.
   for (const s of arr(r.ai_styles)) if (!bad(s)) t.add(titleCase(typeof s === 'string' ? s : s.name));
   for (const s of arr(r.styles)) if (!bad(s)) t.add(titleCase(s));
   for (const p of arr(r.ai_patterns)) if (!bad(p)) t.add(titleCase(typeof p === 'string' ? p : p.name));
-  for (const g of arr(r.ai_tags)) if (!bad(g) && typeof g === 'string') t.add(titleCase(g));
+  // ai_tags carries useful non-color hints (e.g. "Animals","Florals") BUT the scraper also seeded it
+  // with AI palette color-words (Mist/Sky/Duck Egg, Ivory/Stone/Oatmeal) that read as stray-colorway
+  // pollution. Strip a tag iff it exactly matches an AI-palette name — deterministic, not a guess
+  // (TK-11070). The single authoritative colorway is the color: facet above.
+  const palette = new Set(
+    [...arr(r.ai_colors).map(c => c && (c.name || c.color)), r.ai_background_color]
+      .filter(Boolean).map(x => titleCase(x)));
+  for (const g of arr(r.ai_tags)) {
+    if (bad(g) || typeof g !== 'string') continue;
+    const tc = titleCase(g);
+    if (palette.has(tc)) continue;               // drop AI palette color-words
+    t.add(tc);
+  }
   return [...t].filter(Boolean).join(', ');   // always ≥4 (base set) → satisfies 5-field ≥2 tags
 }
 
@@ -125,11 +141,34 @@ function metafields(r) {
   return mf;
 }
 
+// Image selection (TK-11070 — SELECTIVE, not blanket).
+// Sanderson's gallery_images was filled by the scraper with a rolling window of ADJACENT-pattern
+// plate images off the shared collection carousel. Verified against the DB + vendor JSON-LD:
+//   • the vendor product page's JSON-LD "image" declares a SINGLE authoritative hero == this row's
+//     image_url (matches exactly on every fetchable page);
+//   • of the 4,471 gallery entries fleet-wide, 337 embed THIS product's OWN SAW article code in the
+//     filename (e.g. "...DDIW217290_1_SAW0189_02_SANDERSON_101_Dalmatians_WALLPAPER_...") and 337/337
+//     of those match their own row's mfr_sku EXACTLY — they are genuine extra shots of THIS product;
+//   • the remaining ~4,134 carry NO SAW code (or a foreign one) = cross-pattern carousel plates and
+//     CN* paint-chip swatches — pure pollution, NO room-settings.
+// So the reliable, offline, 100%-precise "belongs to this product" signal is: the row's own SAW
+// article code embedded in the filename. Keep hero + own-SAW-coded gallery images; drop all else.
+// (A per-plate numeric-prefix family key was rejected — DABW217232/246/252 share the DABW217 prefix
+//  yet are DIFFERENT patterns, so that heuristic wrongly kept foreign plates.)
+const SAW_IN_NAME = /SAW\s*0*\d{3,4}[-_]\d{1,3}/i;
+function sawKey(s) {                       // normalize "SAW0189-02" | "SAW0189_02" → "SAW018902"
+  const m = String(s || '').match(SAW_IN_NAME);
+  return m ? m[0].toUpperCase().replace(/[^A-Z0-9]/g, '') : null;
+}
 function images(r) {
   const seen = new Set(); const imgs = [];
   const push = u => { if (u && !seen.has(u)) { seen.add(u); imgs.push({ src: u, alt: esc(titleCase(r.pattern_name) || r.mfr_sku) }); } };
-  push(r.image_url);
-  for (const g of arr(r.gallery_images)) push(g);
+  push(r.image_url);                       // vendor-canonical hero — always position 1
+  const own = sawKey(r.mfr_sku);           // this product's own article, e.g. SAW018902
+  for (const g of arr(r.gallery_images)) {
+    // keep ONLY gallery images whose filename embeds THIS product's own SAW code; drop foreign plates.
+    if (own && sawKey(g) === own) push(g);
+  }
   return imgs;
 }
 

← 4e55a40 auto-data-snapshot: 2026-09-01T08:28:16 (2 data files) — scr  ·  back to Designerwallcoverings  ·  Sanderson importer: repair X&Y pattern/color split for color 572262d →