[object Object]

← back to All Designerwallcoverings

all.dw: dedupe grid — suppress sample-twin + cross-source-vendor duplicates (TK-10841)

cb52b7ac67e45b3e14165874a510574c84d58e84 · 2026-08-25 10:16:58 -0700 · Steve

Add dedupeRows() post-concat pass in loadSnapshot(): drop a -sample card only
when its non-sample base twin also renders (sample-only products untouched), and
drop microsite/catalog rows whose vendor is already a live Shopify vendor (incl.
Bespoke->DW Bespoke Studio alias). No DB writes; in-RAM only; git-revertable.
Verified on running :9958: default grid 214,081 -> 188,434, 0 dupes remaining,
38,245 sample-only + Fromental(shopify) preserved.

Files touched

Diff

commit cb52b7ac67e45b3e14165874a510574c84d58e84
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Aug 25 10:16:58 2026 -0700

    all.dw: dedupe grid — suppress sample-twin + cross-source-vendor duplicates (TK-10841)
    
    Add dedupeRows() post-concat pass in loadSnapshot(): drop a -sample card only
    when its non-sample base twin also renders (sample-only products untouched), and
    drop microsite/catalog rows whose vendor is already a live Shopify vendor (incl.
    Bespoke->DW Bespoke Studio alias). No DB writes; in-RAM only; git-revertable.
    Verified on running :9958: default grid 214,081 -> 188,434, 0 dupes remaining,
    38,245 sample-only + Fromental(shopify) preserved.
---
 server.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 50 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index 7475243..663c758 100644
--- a/server.js
+++ b/server.js
@@ -809,6 +809,53 @@ function toInternalRow(d) {
   };
 }
 
+// ── aggregator-level dedup (TK-10841) ──────────────────────────────────────────
+// The grid concatenates 3 sources (shopify_products rows, MICROSITE_ROWS, CATALOG_ROWS)
+// and /api/products renders them 1:1 with no dedup. Two TRUE-duplicate classes result,
+// removed here as a single post-concat pass (in-RAM only; git-revertable; no DB writes):
+//
+//  1. SAMPLE-TWIN — DW models a memo sample as its OWN shopify product whose dw_sku ends
+//     "-sample" (base "488-401" Active + "488-401-sample" Staged, same image/pattern).
+//     Both render as separate cards = visual dupe. We drop a "-sample" row ONLY when a
+//     non-sample row with the SAME bare SKU also renders — so the ~44k sample-ONLY products
+//     (the sample IS the sole card) are untouched. Canonical row = the non-sample one.
+//  2. CROSS-SOURCE VENDOR — a vendor now fully on Shopify (Fromental/Gracie/Zuber) is STILL
+//     injected as microsite rows (the "not in shopify_products" comment on those feeds is
+//     stale), and a catalog line whose name is an alias of a live Shopify vendor ("Bespoke"
+//     ↔ "DW Bespoke Studio") double-lists because the coarse table-level guard is exact-match.
+//     We drop any microsite/catalog row whose (alias-normalized) vendor is already a live
+//     Shopify vendor. Canonical row = the Shopify one.
+//
+// Deliberately NOT deduping by shared image alone: vendors reuse collection/lifestyle imagery
+// across genuinely distinct SKUs (verified — distinct Fromental patterns share one hero image),
+// so image-equality would over-suppress. Identity is SKU + vendor, never image.
+const CROSS_SOURCE_VENDOR_ALIAS = new Map([
+  // catalog/microsite vendor name (alnum-normalized) → the live Shopify vendor it duplicates
+  ['bespoke', 'dwbespokestudio'],
+]);
+function dedupeRows(rows, liveVendorKeys) {
+  const normV = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+  const bareSku = (s) => String(s || '').toLowerCase().replace(/-sample$/, '').trim();
+  const isSample = (s) => /-sample$/i.test(String(s || ''));
+  // base (non-sample) SKUs that render, so a sample twin can be identified. Normalized.
+  const baseSkus = new Set();
+  for (const r of rows) { if (r.sku && !isSample(r.sku)) baseSkus.add(normV(bareSku(r.sku))); }
+  let sampleTwins = 0, crossSourceVendor = 0;
+  const out = rows.filter((r) => {
+    // rule 2 — a microsite/catalog row whose vendor is already a live Shopify vendor (or an alias of one)
+    if (r.source === 'microsite' || r.source === 'catalog') {
+      const vk = normV(r.vendor);
+      const canon = CROSS_SOURCE_VENDOR_ALIAS.get(vk) || vk;
+      if (liveVendorKeys.has(vk) || liveVendorKeys.has(canon)) { crossSourceVendor++; return false; }
+    }
+    // rule 1 — a "-sample" card whose non-sample base sibling also renders
+    if (r.sku && isSample(r.sku) && baseSkus.has(normV(bareSku(r.sku)))) { sampleTwins++; return false; }
+    return true;
+  });
+  console.log(`dedupe: removed ${sampleTwins.toLocaleString()} sample-twin + ${crossSourceVendor.toLocaleString()} cross-source-vendor rows (${rows.length.toLocaleString()} → ${out.length.toLocaleString()})`);
+  return out;
+}
+
 async function loadSnapshot() {
   if (!pool) pool = new Pool({ connectionString: DSN, max: 2 });
   const t0 = Date.now();
@@ -836,13 +883,14 @@ async function loadSnapshot() {
   // Every genuine catalog-only vendor line → additive searchable rows. Non-fatal: on failure
   // CATALOG_ROWS keeps its prior value (or empty on first boot) — the grid is unaffected.
   try { await loadCatalogVendors(liveVendorKeys); } catch (e) { console.error('catalog vendors load failed (non-fatal):', e.message); }
-  ROWS = res.rows
+  ROWS = dedupeRows(res.rows
     .filter((r) => !BANNED.test(r.vendor || '') && !BANNED.test(r.title || ''))
     .map(deriveRow)
     // ...then append the microsite-sourced rows so they're searchable + facetable in the same grid.
     .concat(MICROSITE_ROWS)
     // ...and every genuine catalog-only vendor line, minus the banned private-label mills.
-    .concat(CATALOG_ROWS.filter((r) => !BANNED.test(r.vendor || '') && !BANNED.test(r.title || '')));
+    .concat(CATALOG_ROWS.filter((r) => !BANNED.test(r.vendor || '') && !BANNED.test(r.title || ''))),
+    liveVendorKeys);
   // INTERNAL feed — EVERY non-archived SKU (incl. private-label, NO banned-exclusion), leak-sanitized,
   // plus the extra fields internal consumers (photo + substitute apps) need but the public API omits:
   // product_id (numeric, for Shopify photo push), handle (for product-page URLs), tags (for material/

← 5f3f6eb auto-data-snapshot: 2026-08-24T10:08:33 (1 data files) — pac  ·  back to All Designerwallcoverings  ·  creds-safe fetch guard: resolve relative fetch vs credential ce50840 →