[object Object]

← back to Dw Pairs Well

auto-save: 2026-06-25T23:37:59 (1 files) — tools/

c76eef67c2ff6dcd8c76e6a42e5fec335480e834 · 2026-06-25 23:38:08 -0700 · Steve Abrams

Files touched

Diff

commit c76eef67c2ff6dcd8c76e6a42e5fec335480e834
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jun 25 23:38:08 2026 -0700

    auto-save: 2026-06-25T23:37:59 (1 files) — tools/
---
 tools/pattern-grouping-dryrun.js   | 117 +++++
 tools/pattern-grouping-report.json | 854 +++++++++++++++++++++++++++++++++++++
 tools/pattern-image-verify.py      | 107 +++++
 3 files changed, 1078 insertions(+)

diff --git a/tools/pattern-grouping-dryrun.js b/tools/pattern-grouping-dryrun.js
new file mode 100644
index 0000000..a1ae3af
--- /dev/null
+++ b/tools/pattern-grouping-dryrun.js
@@ -0,0 +1,117 @@
+#!/usr/bin/env node
+// WS-1 dry-run (READ-ONLY): compare candidate pattern-grouping strategies on the
+// live dw_unified mirror so /dtd can commit a grouping-key choice on real numbers.
+// Writes NOTHING. Outputs a report to stdout + tools/pattern-grouping-report.json.
+//
+//   node tools/pattern-grouping-dryrun.js
+//
+// Strategies compared (all scoped WITHIN vendor to avoid cross-vendor collisions):
+//   T0  exact title
+//   T1  normalized title (strip " | vendor" suffix, product-type noise, punctuation)
+//   T2  T1 + strip color tokens (so "Albany Linen-Beige" and "-Cedar" collapse)
+//   PN  pattern_name (baseline — known unreliable; quantifies how bad)
+require('dotenv').config();
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require('pg');
+const { COLORS } = require('../lib/classify');
+
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+
+// product-type / material noise to drop from titles when deriving a pattern key
+const NOISE = [
+  'wallcovering','wallcoverings','wallpaper','wall covering','fabric','fabrics',
+  'durable vinyl','vinyl','grasscloth','print','commercial','residential',
+  'type ii','type iii','mural','wall mural','peel and stick','peel-and-stick',
+  'self adhesive','self-adhesive','by phillipe romano','phillipe romano'
+];
+// color tokens = the classify COLOR set + common modifiers
+const COLOR_WORDS = new Set([...COLORS,
+  'light','dark','deep','pale','soft','warm','cool','muted','bright','antique',
+  'metallic','natural','neutral','multi','multicolor','multicolour'
+]);
+
+function stripVendorSuffix(title) {
+  // "Name ... | Vendor"  → "Name ..."
+  const i = title.indexOf('|');
+  return (i === -1 ? title : title.slice(0, i)).trim();
+}
+function normTitle(title) {
+  let s = stripVendorSuffix(title).toLowerCase();
+  for (const n of NOISE) s = s.split(n).join(' ');
+  s = s.replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
+  return s;
+}
+function stripColors(normalized) {
+  const kept = normalized.split(' ').filter(w => w && !COLOR_WORDS.has(w));
+  return kept.join(' ').trim() || normalized; // never empty-out a title
+}
+
+function summarize(label, keyFn, rows) {
+  const groups = new Map(); // key -> {count, vendor, sampleTitles:Set}
+  let keyed = 0;
+  for (const r of rows) {
+    const k = keyFn(r);
+    if (!k) continue;
+    keyed++;
+    const gk = (r.vendor || '∅') + '::' + k;
+    let g = groups.get(gk);
+    if (!g) { g = { count: 0, vendor: r.vendor, key: k, samples: [] }; groups.set(gk, g); }
+    g.count++;
+    if (g.samples.length < 3) g.samples.push(r.title);
+  }
+  const arr = [...groups.values()];
+  const multi = arr.filter(g => g.count >= 2);
+  const productsInMulti = multi.reduce((a, g) => a + g.count, 0);
+  arr.sort((a, b) => b.count - a.count);
+  return {
+    label,
+    products_keyed: keyed,
+    distinct_groups: arr.length,
+    multi_colorway_groups: multi.length,
+    products_in_multi_groups: productsInMulti,
+    pct_collapsed: ((productsInMulti - multi.length) / keyed * 100).toFixed(1) + '%',
+    avg_group_size: (keyed / arr.length).toFixed(2),
+    top20: arr.slice(0, 20).map(g => ({ n: g.count, vendor: g.vendor, key: g.key, eg: g.samples })),
+    // over-grouping suspects = very large groups (could be legit like Fine Stripes×76 OR junk)
+    huge_groups_gt60: arr.filter(g => g.count > 60).length
+  };
+}
+
+(async () => {
+  console.log('Loading active products from mirror…');
+  const r = await pool.query(`
+    SELECT dw_sku, title, vendor, pattern_name
+    FROM shopify_products
+    WHERE status = 'ACTIVE' AND title IS NOT NULL AND handle IS NOT NULL
+  `);
+  const rows = r.rows;
+  console.log('Active products:', rows.length);
+
+  const reports = [
+    summarize('T0 exact title',            x => stripVendorSuffix(x.title).toLowerCase(), rows),
+    summarize('T1 normalized title',       x => normTitle(x.title), rows),
+    summarize('T2 normalized − colors',    x => stripColors(normTitle(x.title)), rows),
+    summarize('PN pattern_name (baseline)',x => (x.pattern_name || '').toLowerCase().trim() || null, rows)
+  ];
+
+  const out = { generated_at: new Date().toISOString(), active_products: rows.length, strategies: reports };
+  const file = path.join(__dirname, 'pattern-grouping-report.json');
+  fs.writeFileSync(file, JSON.stringify(out, null, 2));
+
+  // ---- console digest ----
+  for (const s of reports) {
+    console.log('\n========================================');
+    console.log(s.label);
+    console.log('  distinct groups        :', s.distinct_groups);
+    console.log('  multi-colorway groups  :', s.multi_colorway_groups);
+    console.log('  products in those      :', s.products_in_multi_groups);
+    console.log('  grid rows removed       :', (s.products_in_multi_groups - s.multi_colorway_groups), '(' + s.pct_collapsed + ' of catalog collapsed)');
+    console.log('  avg group size         :', s.avg_group_size);
+    console.log('  huge groups (>60)      :', s.huge_groups_gt60, '(inspect for over-grouping)');
+    console.log('  top 5 largest:');
+    s.top20.slice(0, 5).forEach(g => console.log(`    ${g.n}× [${g.vendor}] "${g.key}"  e.g. ${JSON.stringify(g.eg[0])}`));
+  }
+  console.log('\nFull report → ', file);
+  await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/tools/pattern-grouping-report.json b/tools/pattern-grouping-report.json
new file mode 100644
index 0000000..2421d75
--- /dev/null
+++ b/tools/pattern-grouping-report.json
@@ -0,0 +1,854 @@
+{
+  "generated_at": "2026-06-26T06:31:26.700Z",
+  "active_products": 74927,
+  "strategies": [
+    {
+      "label": "T0 exact title",
+      "products_keyed": 74873,
+      "distinct_groups": 59710,
+      "multi_colorway_groups": 3388,
+      "products_in_multi_groups": 18551,
+      "pct_collapsed": "20.3%",
+      "avg_group_size": "1.25",
+      "top20": [
+        {
+          "n": 366,
+          "vendor": "Coordonné",
+          "key": "coordonné durable vinyl wallcovering",
+          "eg": [
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+          ]
+        },
+        {
+          "n": 246,
+          "vendor": "Phillipe Romano",
+          "key": "luxuria beige",
+          "eg": [
+            "Luxuria Beige | Phillipe Romano",
+            "Luxuria Beige | Phillipe Romano",
+            "Luxuria Beige | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 216,
+          "vendor": "Surface Stick",
+          "key": "palerma with surface stick - faux wood grain self adhesive",
+          "eg": [
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+          ]
+        },
+        {
+          "n": 149,
+          "vendor": "Roberto Cavalli Wallpaper",
+          "key": "roberto cavalli wallcovering",
+          "eg": [
+            "Roberto Cavalli Wallcovering",
+            "Roberto Cavalli Wallcovering",
+            "Roberto Cavalli Wallcovering"
+          ]
+        },
+        {
+          "n": 138,
+          "vendor": "Surface Stick",
+          "key": "elegante with surface stick - faux fine wood self adhesive",
+          "eg": [
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+          ]
+        },
+        {
+          "n": 112,
+          "vendor": "Surface Stick",
+          "key": "santiago with surface stick - self adhesive",
+          "eg": [
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive"
+          ]
+        },
+        {
+          "n": 98,
+          "vendor": "Versace",
+          "key": "versace wallcovering",
+          "eg": [
+            "Versace Wallcovering",
+            "Versace Wallcovering",
+            "Versace Wallcovering"
+          ]
+        },
+        {
+          "n": 95,
+          "vendor": "Hospitality Wallcoverings",
+          "key": "quick ship durable walls",
+          "eg": [
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls"
+          ]
+        },
+        {
+          "n": 78,
+          "vendor": "Designer Wallcoverings",
+          "key": "ralph mohair velvet from netherlands, holland",
+          "eg": [
+            "Ralph Mohair Velvet from Netherlands, Holland",
+            "Ralph Mohair Velvet from Netherlands, Holland",
+            "Ralph Mohair Velvet from Netherlands, Holland"
+          ]
+        },
+        {
+          "n": 74,
+          "vendor": "Phillipe Romano",
+          "key": "phillipe romano - renaissance metal leaf",
+          "eg": [
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano - Renaissance Metal Leaf"
+          ]
+        },
+        {
+          "n": 74,
+          "vendor": "Hollywood Wallcoverings",
+          "key": "ashbourne type ii vinyl",
+          "eg": [
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Designer Wallcoverings",
+          "key": "woodside wood walls",
+          "eg": [
+            "Woodside Wood Walls",
+            "Woodside Wood Walls",
+            "Woodside Wood Walls"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Phillipe Romano",
+          "key": "hollywood abaca grasscloth",
+          "eg": [
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Phillipe Romano",
+          "key": "fine stripes by phillipe romano",
+          "eg": [
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano"
+          ]
+        },
+        {
+          "n": 68,
+          "vendor": "Designer Wallcoverings",
+          "key": "torre wallcovering prints",
+          "eg": [
+            "Torre Wallcovering Prints",
+            "Torre Wallcovering Prints",
+            "Torre Wallcovering Prints"
+          ]
+        },
+        {
+          "n": 62,
+          "vendor": "Koroseal",
+          "key": "wallcovering",
+          "eg": [
+            "Wallcovering | Sarah Rowland | Architectural Wallcoverings",
+            "Wallcovering | Architectural Wallcoverings",
+            "Wallcovering | Sarah Rowland | Architectural Wallcoverings"
+          ]
+        },
+        {
+          "n": 60,
+          "vendor": "Schumacher",
+          "key": "schumacher trim",
+          "eg": [
+            "Schumacher Trim",
+            "Schumacher Trim",
+            "Schumacher Trim"
+          ]
+        },
+        {
+          "n": 55,
+          "vendor": "Phillipe Romano",
+          "key": "luxuria light gray",
+          "eg": [
+            "Luxuria Light Gray | Phillipe Romano",
+            "Luxuria Light Gray | Phillipe Romano",
+            "Luxuria Light Gray | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 54,
+          "vendor": "Phillipe Romano",
+          "key": "grand dame by pr",
+          "eg": [
+            "Grand Dame by Pr",
+            "Grand Dame by Pr",
+            "Grand Dame by Pr"
+          ]
+        },
+        {
+          "n": 54,
+          "vendor": "Newmor Wallcoverings",
+          "key": "cezanne commercial wallcovering",
+          "eg": [
+            "Cezanne Commercial Wallcovering",
+            "Cezanne Commercial Wallcovering",
+            "Cezanne Commercial Wallcovering"
+          ]
+        }
+      ],
+      "huge_groups_gt60": 16
+    },
+    {
+      "label": "T1 normalized title",
+      "products_keyed": 74801,
+      "distinct_groups": 59130,
+      "multi_colorway_groups": 3845,
+      "products_in_multi_groups": 19516,
+      "pct_collapsed": "21.0%",
+      "avg_group_size": "1.27",
+      "top20": [
+        {
+          "n": 374,
+          "vendor": "Coordonné",
+          "key": "coordonn",
+          "eg": [
+            "Coordonné | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+          ]
+        },
+        {
+          "n": 246,
+          "vendor": "Phillipe Romano",
+          "key": "luxuria beige",
+          "eg": [
+            "Luxuria Beige | Phillipe Romano",
+            "Luxuria Beige | Phillipe Romano",
+            "Luxuria Beige | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 216,
+          "vendor": "Surface Stick",
+          "key": "palerma with surface stick faux wood grain",
+          "eg": [
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+          ]
+        },
+        {
+          "n": 149,
+          "vendor": "Roberto Cavalli Wallpaper",
+          "key": "roberto cavalli",
+          "eg": [
+            "Roberto Cavalli Wallcovering",
+            "Roberto Cavalli Wallcovering",
+            "Roberto Cavalli Wallcovering"
+          ]
+        },
+        {
+          "n": 138,
+          "vendor": "Surface Stick",
+          "key": "elegante with surface stick faux fine wood",
+          "eg": [
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+          ]
+        },
+        {
+          "n": 112,
+          "vendor": "Surface Stick",
+          "key": "santiago with surface stick",
+          "eg": [
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive"
+          ]
+        },
+        {
+          "n": 98,
+          "vendor": "Versace",
+          "key": "versace",
+          "eg": [
+            "Versace Wallcovering",
+            "Versace Wallcovering",
+            "Versace Wallcovering"
+          ]
+        },
+        {
+          "n": 95,
+          "vendor": "Hospitality Wallcoverings",
+          "key": "quick ship durable walls",
+          "eg": [
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls"
+          ]
+        },
+        {
+          "n": 78,
+          "vendor": "Designer Wallcoverings",
+          "key": "ralph mohair velvet from netherlands holland",
+          "eg": [
+            "Ralph Mohair Velvet from Netherlands, Holland",
+            "Ralph Mohair Velvet from Netherlands, Holland",
+            "Ralph Mohair Velvet from Netherlands, Holland"
+          ]
+        },
+        {
+          "n": 74,
+          "vendor": "Phillipe Romano",
+          "key": "renaissance metal leaf",
+          "eg": [
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano - Renaissance Metal Leaf"
+          ]
+        },
+        {
+          "n": 74,
+          "vendor": "Hollywood Wallcoverings",
+          "key": "ashbourne",
+          "eg": [
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Designer Wallcoverings",
+          "key": "woodside wood walls",
+          "eg": [
+            "Woodside Wood Walls",
+            "Woodside Wood Walls",
+            "Woodside Wood Walls"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Phillipe Romano",
+          "key": "hollywood abaca",
+          "eg": [
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Phillipe Romano",
+          "key": "fine stripes",
+          "eg": [
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano"
+          ]
+        },
+        {
+          "n": 68,
+          "vendor": "Designer Wallcoverings",
+          "key": "torre s",
+          "eg": [
+            "Torre Wallcovering Prints",
+            "Torre Wallcovering Prints",
+            "Torre Wallcovering Prints"
+          ]
+        },
+        {
+          "n": 65,
+          "vendor": "Hollywood Wallcoverings",
+          "key": "faux leaf squares",
+          "eg": [
+            "Faux Leaf Squares | Hollywood Wallcoverings",
+            "Faux Leaf Squares | Hollywood Wallcoverings",
+            "Faux Leaf Squares Wallcovering | Hollywood Wallcoverings"
+          ]
+        },
+        {
+          "n": 60,
+          "vendor": "Schumacher",
+          "key": "schumacher trim",
+          "eg": [
+            "Schumacher Trim",
+            "Schumacher Trim",
+            "Schumacher Trim"
+          ]
+        },
+        {
+          "n": 55,
+          "vendor": "Phillipe Romano",
+          "key": "luxuria light gray",
+          "eg": [
+            "Luxuria Light Gray | Phillipe Romano",
+            "Luxuria Light Gray | Phillipe Romano",
+            "Luxuria Light Gray | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 54,
+          "vendor": "Phillipe Romano",
+          "key": "grand dame by pr",
+          "eg": [
+            "Grand Dame by Pr",
+            "Grand Dame by Pr",
+            "Grand Dame by Pr"
+          ]
+        },
+        {
+          "n": 54,
+          "vendor": "Newmor Wallcoverings",
+          "key": "cezanne",
+          "eg": [
+            "Cezanne Commercial Wallcovering",
+            "Cezanne Commercial Wallcovering",
+            "Cezanne Commercial Wallcovering"
+          ]
+        }
+      ],
+      "huge_groups_gt60": 16
+    },
+    {
+      "label": "T2 normalized − colors",
+      "products_keyed": 74801,
+      "distinct_groups": 49844,
+      "multi_colorway_groups": 7416,
+      "products_in_multi_groups": 32373,
+      "pct_collapsed": "33.4%",
+      "avg_group_size": "1.50",
+      "top20": [
+        {
+          "n": 594,
+          "vendor": "Phillipe Romano",
+          "key": "luxuria",
+          "eg": [
+            "Luxuria Beige | Phillipe Romano",
+            "Luxuria Beige | Phillipe Romano",
+            "Luxuria Khaki | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 374,
+          "vendor": "Coordonné",
+          "key": "coordonn",
+          "eg": [
+            "Coordonné | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+          ]
+        },
+        {
+          "n": 216,
+          "vendor": "Surface Stick",
+          "key": "palerma with surface stick faux wood grain",
+          "eg": [
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+          ]
+        },
+        {
+          "n": 149,
+          "vendor": "Roberto Cavalli Wallpaper",
+          "key": "roberto cavalli",
+          "eg": [
+            "Roberto Cavalli Wallcovering",
+            "Roberto Cavalli Wallcovering",
+            "Roberto Cavalli Wallcovering"
+          ]
+        },
+        {
+          "n": 143,
+          "vendor": "Kravet",
+          "key": "kravet design",
+          "eg": [
+            "Kravet Design - Beige Commercial Wallcovering | Kravet",
+            "Kravet Design - Beige Wallcovering | Kravet",
+            "Kravet Design - Taupe Natural Wallcovering | Kravet"
+          ]
+        },
+        {
+          "n": 138,
+          "vendor": "Surface Stick",
+          "key": "elegante with surface stick faux fine wood",
+          "eg": [
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+          ]
+        },
+        {
+          "n": 112,
+          "vendor": "Surface Stick",
+          "key": "santiago with surface stick",
+          "eg": [
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive"
+          ]
+        },
+        {
+          "n": 109,
+          "vendor": "Nina Campbell",
+          "key": "nina campbell",
+          "eg": [
+            "Nina Campbell Fabric - Cool Neutral Wallcovering | Nina Campbell",
+            "Nina Campbell Fabric - Peach Beige Wallcovering | Nina Campbell",
+            "Nina Campbell Fabric - Teal Blue Wallcovering | Nina Campbell"
+          ]
+        },
+        {
+          "n": 98,
+          "vendor": "Versace",
+          "key": "versace",
+          "eg": [
+            "Versace Wallcovering",
+            "Versace Wallcovering",
+            "Versace Wallcovering"
+          ]
+        },
+        {
+          "n": 95,
+          "vendor": "Hospitality Wallcoverings",
+          "key": "quick ship durable walls",
+          "eg": [
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls"
+          ]
+        },
+        {
+          "n": 78,
+          "vendor": "Designer Wallcoverings",
+          "key": "ralph mohair velvet from netherlands holland",
+          "eg": [
+            "Ralph Mohair Velvet from Netherlands, Holland",
+            "Ralph Mohair Velvet from Netherlands, Holland",
+            "Ralph Mohair Velvet from Netherlands, Holland"
+          ]
+        },
+        {
+          "n": 74,
+          "vendor": "Phillipe Romano",
+          "key": "renaissance metal leaf",
+          "eg": [
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano - Renaissance Metal Leaf"
+          ]
+        },
+        {
+          "n": 74,
+          "vendor": "Hollywood Wallcoverings",
+          "key": "ashbourne",
+          "eg": [
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Designer Wallcoverings",
+          "key": "woodside wood walls",
+          "eg": [
+            "Woodside Wood Walls",
+            "Woodside Wood Walls",
+            "Woodside Wood Walls"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Phillipe Romano",
+          "key": "hollywood abaca",
+          "eg": [
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Phillipe Romano",
+          "key": "fine stripes",
+          "eg": [
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano"
+          ]
+        },
+        {
+          "n": 68,
+          "vendor": "Designer Wallcoverings",
+          "key": "torre s",
+          "eg": [
+            "Torre Wallcovering Prints",
+            "Torre Wallcovering Prints",
+            "Torre Wallcovering Prints"
+          ]
+        },
+        {
+          "n": 67,
+          "vendor": "Daisy Bennett",
+          "key": "daisy bennett",
+          "eg": [
+            "Daisy Bennett - White Wallcovering | Daisy Bennett",
+            "Daisy Bennett - Beige Wallcovering | Daisy Bennett",
+            "Daisy Bennett - White Wallcovering | Daisy Bennett"
+          ]
+        },
+        {
+          "n": 65,
+          "vendor": "Hollywood Wallcoverings",
+          "key": "faux leaf squares",
+          "eg": [
+            "Faux Leaf Squares | Hollywood Wallcoverings",
+            "Faux Leaf Squares | Hollywood Wallcoverings",
+            "Faux Leaf Squares Wallcovering | Hollywood Wallcoverings"
+          ]
+        },
+        {
+          "n": 60,
+          "vendor": "Schumacher",
+          "key": "schumacher trim",
+          "eg": [
+            "Schumacher Trim",
+            "Schumacher Trim",
+            "Schumacher Trim"
+          ]
+        }
+      ],
+      "huge_groups_gt60": 19
+    },
+    {
+      "label": "PN pattern_name (baseline)",
+      "products_keyed": 59854,
+      "distinct_groups": 26614,
+      "multi_colorway_groups": 6777,
+      "products_in_multi_groups": 40017,
+      "pct_collapsed": "55.5%",
+      "avg_group_size": "2.25",
+      "top20": [
+        {
+          "n": 1519,
+          "vendor": "Kravet",
+          "key": "kravet design",
+          "eg": [
+            "Kravet Design - W3721-12 Orange Wallcovering | Kravet",
+            "Kravet Design - W4111-11 Slate Wallcovering | Kravet",
+            "Kravet Design - W4154-11 Grey Wallcovering | Kravet"
+          ]
+        },
+        {
+          "n": 660,
+          "vendor": "Phillipe Romano",
+          "key": "luxuria fine textile",
+          "eg": [
+            "Luxuria #d5d8c9 | Phillipe Romano",
+            "Luxuria Beige | Phillipe Romano",
+            "Luxuria Golden Brown | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 366,
+          "vendor": "Coordonné",
+          "key": "coordonné durable vinyl wallcovering",
+          "eg": [
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe",
+            "Coordonné Durable Vinyl Wallcovering | Coordone Europe"
+          ]
+        },
+        {
+          "n": 216,
+          "vendor": "Surface Stick",
+          "key": "palerma with surface stick",
+          "eg": [
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive",
+            "Palerma with Surface Stick - Faux Wood Grain Self Adhesive"
+          ]
+        },
+        {
+          "n": 138,
+          "vendor": "Surface Stick",
+          "key": "elegante with surface stick",
+          "eg": [
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive",
+            "Elegante with Surface Stick - Faux Fine Wood Self Adhesive"
+          ]
+        },
+        {
+          "n": 115,
+          "vendor": "Novasuede",
+          "key": "novasuede™ fabric & wallcovering",
+          "eg": [
+            "Novasuede™ - Trench Luxury Suede",
+            "Novasuede™ - Taupe Luxury Suede",
+            "Novasuede™ Fabric & Wallcovering - White"
+          ]
+        },
+        {
+          "n": 112,
+          "vendor": "Surface Stick",
+          "key": "santiago with surface stick",
+          "eg": [
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive",
+            "Santiago with Surface Stick - Self Adhesive"
+          ]
+        },
+        {
+          "n": 105,
+          "vendor": "Marburg",
+          "key": "papis loveday wallcovering",
+          "eg": [
+            "Papis Loveday - Beige, Pearl Wallcovering | Marburg",
+            "Papis Loveday Fashion Icon - Gold, Silver Wallcovering | Marburg",
+            "Papis Loveday Fashion Icon - Blue Wallcovering | Marburg"
+          ]
+        },
+        {
+          "n": 101,
+          "vendor": "Clarke And Clarke",
+          "key": "alvar",
+          "eg": [
+            "Alvar - Snow  By Clarke And Clarke | Clarke & Clarke Alvar |Solid Texture Multipurpose Velvet",
+            "Alvar - Charcoal  By Clarke And Clarke | Clarke & Clarke Alvar |Solid Texture Multipurpose Velvet",
+            "Alvar - Mist  By Clarke And Clarke | Clarke & Clarke Alvar |Solid Texture Multipurpose Velvet"
+          ]
+        },
+        {
+          "n": 98,
+          "vendor": "Kravet",
+          "key": "ultrasuede green",
+          "eg": [
+            "Ultrasuede™ Green - Cadet Blue By Kravet Design | Performance |Solid Texture Upholstery Vinyl/Faux Leather / Vegan Leather & Fabric",
+            "Ultrasuede™ Green - Chino Beige By Kravet Design | Performance |Solid Texture Upholstery Vinyl/Faux Leather / Vegan Leather & Fabric",
+            "Ultrasuede™ Green - Flannel Brown By Kravet Design | Performance |Solid Texture Upholstery Vinyl/Faux Leather / Vegan Leather & Fabric"
+          ]
+        },
+        {
+          "n": 89,
+          "vendor": "Brunschwig & Fils",
+          "key": "chevalier wool",
+          "eg": [
+            "Chevalier Wool - Tiger'S Eye Brown By Brunschwig & Fils |  |Solid Texture Upholstery Wool",
+            "Chevalier Wool - Saffron Yellow By Brunschwig & Fils |  |Solid Texture Upholstery Wool",
+            "Chevalier Wool - Pomegranate Burgundy/Red By Brunschwig & Fils |  |Solid Texture Upholstery Wool"
+          ]
+        },
+        {
+          "n": 86,
+          "vendor": "Glitter Walls",
+          "key": "hollywood glamour sequin",
+          "eg": [
+            "Hollywood Glamour Sequin - Lime Iris | Glitter Walls",
+            "Hollywood Glamour Sequin - Sand | Glitter Walls",
+            "Hollywood Glamour Sequin - Bronze | Glitter Walls"
+          ]
+        },
+        {
+          "n": 79,
+          "vendor": "Gaston Y Daniela",
+          "key": "palma",
+          "eg": [
+            "Palma - Verde Billar Green By Gaston Y Daniela | Gaston Maiorica |Solid  Upholstery",
+            "Palma - Anil Blue By Gaston Y Daniela | Gaston Maiorica |Solid  Upholstery",
+            "Palma - Barquillo Wheat By Gaston Y Daniela | Gaston Maiorica |Solid  Upholstery"
+          ]
+        },
+        {
+          "n": 78,
+          "vendor": "Phillipe Romano",
+          "key": "phillipe romano",
+          "eg": [
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano - Renaissance Metal Leaf",
+            "Phillipe Romano Commercial"
+          ]
+        },
+        {
+          "n": 74,
+          "vendor": "Hollywood Wallcoverings",
+          "key": "ashbourne type ii vinyl",
+          "eg": [
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings",
+            "Ashbourne Type II Vinyl | Hollywood Wallcoverings"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Designer Wallcoverings",
+          "key": "woodside wood",
+          "eg": [
+            "Woodside Wood Walls",
+            "Woodside Wood Walls",
+            "Woodside Wood Walls"
+          ]
+        },
+        {
+          "n": 73,
+          "vendor": "Phillipe Romano",
+          "key": "hollywood abaca grasscloth",
+          "eg": [
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano",
+            "Hollywood Abaca Grasscloth | Phillipe Romano"
+          ]
+        },
+        {
+          "n": 72,
+          "vendor": "Hospitality Wallcoverings",
+          "key": "quick ship durable",
+          "eg": [
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls",
+            "Quick Ship Durable Walls"
+          ]
+        },
+        {
+          "n": 72,
+          "vendor": "Phillipe Romano",
+          "key": "fine stripes",
+          "eg": [
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano",
+            "Fine Stripes by Phillipe Romano"
+          ]
+        },
+        {
+          "n": 69,
+          "vendor": "Clarke And Clarke",
+          "key": "highlander",
+          "eg": [
+            "Highlander - Cinnamon  By Clarke And Clarke | Clarke & Clarke Highlander 2 |Solid Texture Multipurpose",
+            "Highlander - Antique  By Clarke And Clarke | Clarke & Clarke Highlander 2 |Solid Texture Multipurpose",
+            "Highlander - Berry  By Clarke And Clarke | Clarke & Clarke Highlander |Solid Texture Multipurpose"
+          ]
+        }
+      ],
+      "huge_groups_gt60": 24
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tools/pattern-image-verify.py b/tools/pattern-image-verify.py
new file mode 100644
index 0000000..8f843cb
--- /dev/null
+++ b/tools/pattern-image-verify.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""WS-1 image confirmation (READ-ONLY, local, $0).
+
+Reads "dw_sku<TAB>title<TAB>image_url" lines on stdin (one candidate group),
+fetches each image, computes a COLOR-INVARIANT structural signature, and clusters
+members by visual pattern. Same pattern in different colorways -> one cluster;
+genuinely different patterns sharing a key (e.g. a templated-title vendor) ->
+multiple clusters. This is the "are these really the same pattern, just different
+colors?" confirmation on top of the SKU/title key.
+
+Color invariance: image -> grayscale -> we compute dHash on BOTH the image and its
+tonal inverse and take the smaller Hamming distance, so a light-on-dark vs
+dark-on-light colorway of the same motif still matches.
+
+  psql ... -tAc "<query yielding sku|title|url>" | tr '|' '\\t' | \
+     python3 tools/pattern-image-verify.py "Coordonné" 30
+"""
+import sys, io, urllib.request, hashlib, os
+from PIL import Image, ImageOps
+
+LABEL = sys.argv[1] if len(sys.argv) > 1 else "group"
+LIMIT = int(sys.argv[2]) if len(sys.argv) > 2 else 40
+THRESH = int(os.environ.get("HAM_THRESH", "12"))   # dHash Hamming <= THRESH => same pattern
+HASH = 8
+CACHE = "/tmp/pattern-verify-cache"
+os.makedirs(CACHE, exist_ok=True)
+
+def dhash(img, n=HASH):
+    g = img.convert("L").resize((n + 1, n), Image.LANCZOS)
+    px = list(g.getdata())
+    v = 0
+    for r in range(n):
+        base = r * (n + 1)
+        for c in range(n):
+            v = (v << 1) | (1 if px[base + c] > px[base + c + 1] else 0)
+    return v
+
+def inv_dhash(img, n=HASH):
+    return dhash(ImageOps.invert(img.convert("L")), n)
+
+def ham(a, b):
+    return bin(a ^ b).count("1")
+
+def fetch(url):
+    key = os.path.join(CACHE, hashlib.md5(url.encode()).hexdigest() + ".bin")
+    if os.path.exists(key):
+        with open(key, "rb") as f:
+            return f.read()
+    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 dw-pattern-verify"})
+    data = urllib.request.urlopen(req, timeout=20).read()
+    with open(key, "wb") as f:
+        f.write(data)
+    return data
+
+items = []
+for line in sys.stdin:
+    parts = line.rstrip("\n").split("\t")
+    if len(parts) < 3:
+        continue
+    sku, title, url = parts[0], parts[1], parts[2]
+    if not url.startswith("http"):
+        continue
+    items.append((sku, title, url))
+    if len(items) >= LIMIT:
+        break
+
+print(f"=== {LABEL}: {len(items)} candidate members (thresh dHash<={THRESH}) ===")
+hashed = []
+for sku, title, url in items:
+    try:
+        im = Image.open(io.BytesIO(fetch(url)))
+        hashed.append((sku, title, dhash(im), inv_dhash(im)))
+    except Exception as e:
+        print(f"  [skip] {sku}: {e}")
+
+# single-linkage clustering: a member joins a cluster if it's within THRESH of ANY member
+clusters = []
+for sku, title, h, hi in hashed:
+    placed = False
+    for cl in clusters:
+        for (_, _, ch, _chi) in cl:
+            if min(ham(h, ch), ham(hi, ch)) <= THRESH:
+                cl.append((sku, title, h, hi))
+                placed = True
+                break
+        if placed:
+            break
+    if not placed:
+        clusters.append([(sku, title, h, hi)])
+
+clusters.sort(key=len, reverse=True)
+print(f"  -> {len(hashed)} images formed {len(clusters)} distinct visual pattern(s)")
+for i, cl in enumerate(clusters[:8], 1):
+    egs = "; ".join(t for (_, t, _, _) in cl[:3])
+    print(f"   cluster {i}: {len(cl):>3} member(s)  e.g. {egs[:90]}")
+if len(clusters) > 8:
+    print(f"   … +{len(clusters)-8} more clusters")
+
+# verdict for this candidate group
+if hashed:
+    biggest = len(clusters[0]) / len(hashed)
+    if len(clusters) == 1:
+        print("  VERDICT: CONFIRMED single pattern — safe to group as colorways.")
+    elif biggest >= 0.6:
+        print(f"  VERDICT: MOSTLY one pattern ({len(clusters[0])}/{len(hashed)}) + {len(clusters)-1} outlier(s) to split off.")
+    else:
+        print(f"  VERDICT: OVER-GROUPED — {len(clusters)} real patterns share this key. Do NOT group by key alone.")

← 5e32b37 Add /api/similar (visually-similar designs) + similar-design  ·  back to Dw Pairs Well  ·  Add pattern-id-backfill dry-run (Option-A hybrid) + texture- 90dbcaf →