[object Object]

← back to Designer Wallcoverings

cadence-import: GUARD-ONLY denylist holds banned front-facing Brand (Brewster/York) before publish

b1b593b1b790a54c75d1b36cd3e93e3e97a769f7 · 2026-06-25 10:02:28 -0700 · Steve Abrams

Files touched

Diff

commit b1b593b1b790a54c75d1b36cd3e93e3e97a769f7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jun 25 10:02:28 2026 -0700

    cadence-import: GUARD-ONLY denylist holds banned front-facing Brand (Brewster/York) before publish
---
 shopify/scripts/cadence/cadence-import.js | 69 ++++++++++++++++++++++++++++---
 1 file changed, 64 insertions(+), 5 deletions(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index aefb3943..59e7e353 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -137,6 +137,41 @@ function scrubBanned(str) {
     .replace(/\bCarlsten\b/gi, '');                   // Desima/Carlsten line
   return s.replace(/\s{2,}/g, ' ').trim();
 }
+// Importer DENYLIST GUARD (GUARD-ONLY, Steve-approved 2026-06-25, DWBR source re-leak).
+// Resolves the Brand/vendor LABEL the importer would apply (the VENDORS-map key, which
+// becomes BOTH the title suffix `… | <vendor>` AND the global.Brand metafield), runs it
+// through the SAME banned-name denylist as scrubBanned()/dw-leak-scanner, and if it would
+// stamp a banned FRONT-FACING name (Brewster, York Wallcoverings, "Brewster & York", and the
+// other private-label upstreams), returns a reason string so the caller HOLDS/SKIPS the
+// product instead of publishing it. Returns null for clean vendors (pass-through unchanged).
+// Belt-and-suspenders: a banned Brand label can never reach Shopify even if vendor_registry /
+// vendors.js drifts back to a banned front-facing name. This is the SOURCE stop; the remap
+// (vendor_registry house-line mapping) is deferred per Steve's GUARD-ONLY decision.
+function bannedBrandReason(label) {
+  const s = String(label == null ? '' : label);
+  // Each entry: human reason ⇐ regex that matches the banned front-facing name.
+  // Mirrors scrubBanned()'s denylist but applied to the WHOLE Brand label (a banned label is
+  // one the scrubber would gut), and tuned to NOT trip on legitimate vendor names that merely
+  // CONTAIN a safe word (e.g. "New York Night" pattern, "York Weave"): we only match the
+  // private-label upstream / trade-parent names, not bare "York".
+  const RULES = [
+    [/Brewster\s*&(?:amp;)*\s*York/i,    '"Brewster & York" is a banned front-facing trade-parent name'],
+    [/\bBrewster\b/i,                    '"Brewster" is a banned front-facing trade-parent name'],
+    [/\bYork Wallcoverings?\b/i,         '"York Wallcoverings" is a banned front-facing trade-parent name'],
+    [/\bby York\b/i,                     '"by York" is a banned front-facing trade-parent name'],
+    [/\byorkwall\b/i,                    '"yorkwall" (York trade-portal) is a banned front-facing name'],
+    [/\bWall ?Quest\b/i,                 '"WallQuest" is a banned private-label upstream (→ Malibu Wallpaper)'],
+    [/\bChesapeake\b/i,                  '"Chesapeake" is a banned WallQuest sub-brand'],
+    [/\bNext ?Wall\b/i,                  '"NextWall" is a banned WallQuest sub-brand'],
+    [/\bSeabrook\b/i,                    '"Seabrook" is a banned WallQuest sub-brand'],
+    [/\bCommand ?54\b/i,                 '"Command54" is a banned private-label upstream (→ Phillipe Romano)'],
+    [/\bDesima\b/i,                      '"Desima" is a banned private-label upstream (→ Phillipe Romano Elegante)'],
+    [/\bCarlsten\b/i,                    '"Carlsten" is a banned Desima/Carlsten line name'],
+  ];
+  for (const [re, why] of RULES) { if (re.test(s)) return why; }
+  return null;
+}
+
 // Defensive unwrap for double-encoded metafield values. If a catalog spec column
 // (e.g. material) holds a whole Shopify metafield object — {"type":...,"value":"X"} —
 // stored as its JSON string, writing it raw leaks the blob into BOTH custom.material
@@ -637,6 +672,20 @@ async function findExistingProduct(dwSku) {
 }
 
 async function createProduct(table, row, payload) {
+  // DENYLIST GUARD (GUARD-ONLY, 2026-06-25) — before ANY write or DB price-log, resolve the
+  // Brand/vendor label this product would stamp (vendor key = title suffix + global.Brand) and
+  // HOLD/SKIP it if that label is a banned front-facing name. Belt-and-suspenders: also re-check
+  // the title and the global.Brand metafield the payload actually carries, in case a future code
+  // path sets Brand from somewhere other than the vendor key. Logged (never silently dropped),
+  // and the DB price-log is intentionally skipped so a held product leaves no partial trace.
+  const brandMf = (payload.input?.metafields || []).find(m => m.namespace === 'global' && m.key === 'Brand');
+  const hold = bannedBrandReason(payload.input?.vendor)
+            || bannedBrandReason(brandMf && brandMf.value)
+            || bannedBrandReason(payload.input?.title);
+  if (hold) {
+    console.log(`  ⛔ HELD ${row.dw_sku} — ${hold} | resolved Brand="${payload.input?.vendor}" title="${String(payload.title||'').slice(0,60)}"`);
+    return { ok: false, err: `HELD: banned front-facing Brand (${hold})`, held: true };
+  }
   // DEDUP GUARD — never create a second copy of a sku that already has a live listing.
   const existing = await findExistingProduct(row.dw_sku);
   if (existing && !existing.archivedOnly) {
@@ -681,7 +730,7 @@ async function createProduct(table, row, payload) {
 
 // Export internals for unit/inspection harnesses (no behavior change when run as a script;
 // the main IIFE below only runs on direct execution via the require.main guard).
-module.exports = { gate, selectSkus, buildInput, buildSampleOnlyInput, brandFilterClause, scrubBanned };
+module.exports = { gate, selectSkus, buildInput, buildSampleOnlyInput, brandFilterClause, scrubBanned, bannedBrandReason };
 
 // ---- main ----
 if (require.main === module) (async () => {
@@ -734,7 +783,7 @@ if (require.main === module) (async () => {
   for (let i=0;i<picks;i++) picked.push(ready[(start+i)%ready.length]);
   console.log(`\n=== ${COMMIT?'LIVE COMMIT':'DRY-RUN'} — slot ${SLOT}: lead=${ready[start].vendor} · ${picked.length}/${ready.length} READY vendor(s) × up to ${SKUS_PER_VENDOR} SKUs (cursor idx ${cur.idx}→${(start+picked.length)%ready.length}) ===`);
 
-  const plan = []; let created=0, failed=0, linked=0, activatedCount=0; const activatedSkus=[]; const publishedProductIds=[]; let hitMax=false, hitCap=false;
+  const plan = []; let created=0, failed=0, linked=0, held=0, activatedCount=0; const activatedSkus=[]; const publishedProductIds=[]; let hitMax=false, hitCap=false;
   if (MAX_PRODUCTS) console.log(`(--max ${MAX_PRODUCTS} products this run)${ACTIVATE?'  --activate: readiness-passing → ACTIVE + inventory 2026':''}`);
   for (const r of picked) {
     if (hitMax || hitCap) break;
@@ -746,14 +795,24 @@ if (require.main === module) (async () => {
       // MAP-priced vendors (sellExpr) sell at MAP; everyone else at cost/0.65/0.85.
       const retail = r.cfg.sampleOnly ? 0 : ((r.cfg.sellExpr && row.sell > 0) ? Math.round(row.sell * 100) / 100 : RETAIL(row.cost));
       const payload = buildInput(r.vendor, r.cfg, row, retail, ACTIVATE);
-      plan.push({ vendor:r.vendor, dw_sku:row.dw_sku, cost:row.cost, retail, sample:SAMPLE_PRICE, sampleOnly: !!r.cfg.sampleOnly, title:payload.title, willActivate:payload.willActivate });
+      // DENYLIST GUARD (GUARD-ONLY, 2026-06-25) — resolve the Brand/vendor label this product
+      // would stamp and HOLD it if banned. Surfaced in BOTH dry-run and commit so a held item is
+      // always visible (never silently dropped). createProduct() enforces the same guard at the
+      // write chokepoint; this front check makes the HOLD visible in the plan + dry-run output.
+      const brandMfPre = (payload.input?.metafields || []).find(m => m.namespace === 'global' && m.key === 'Brand');
+      const heldReason = bannedBrandReason(payload.input?.vendor)
+                      || bannedBrandReason(brandMfPre && brandMfPre.value)
+                      || bannedBrandReason(payload.title);
+      plan.push({ vendor:r.vendor, dw_sku:row.dw_sku, cost:row.cost, retail, sample:SAMPLE_PRICE, sampleOnly: !!r.cfg.sampleOnly, title:payload.title, willActivate:payload.willActivate, ...(heldReason ? { held:true, heldReason } : {}) });
+      if (heldReason) { console.log(`  ⛔ HELD ${row.dw_sku} — ${heldReason} | Brand="${payload.input?.vendor}" | ${String(payload.title||'').slice(0,52)}`); continue; }
       if (!COMMIT) {
         if (r.cfg.sampleOnly) console.log(`  · ${row.dw_sku}  SAMPLE-ONLY  sample $${SAMPLE_PRICE}  ${ACTIVATE?(payload.willActivate?'[→ACTIVE]':'[draft:not-ready]'):''}  | ${payload.title.slice(0,52)}`);
         else console.log(`  · ${row.dw_sku}  cost $${row.cost.toFixed(2)} → roll $${retail}  sample $${SAMPLE_PRICE}  ${ACTIVATE?(payload.willActivate?'[→ACTIVE]':'[draft:not-ready]'):''}  | ${payload.title.slice(0,48)}`);
         continue;
       }
       const res = await createProduct(r.cfg.table, row, payload);
-      if (res.ok && res.action === 'linked-existing') { linked++; console.log(`  ↪ ${row.dw_sku} already on Shopify (${res.status}) → backfilled link ${String(res.pid).replace(/.*\//,'')}, skipped create (dedup guard)`); }
+      if (res.held) { held++; /* HELD already logged inside createProduct; counted, never published */ }
+      else if (res.ok && res.action === 'linked-existing') { linked++; console.log(`  ↪ ${row.dw_sku} already on Shopify (${res.status}) → backfilled link ${String(res.pid).replace(/.*\//,'')}, skipped create (dedup guard)`); }
       else if (res.ok) { created++; if (res.activated) activatedCount++; if (res.activated && res.skus) activatedSkus.push(...res.skus); if (res.published && res.pid) publishedProductIds.push(res.pid); if(created%10===0) process.stdout.write(`\r  created ${created}…`); }
       else { failed++; console.log(`  ✗ ${row.dw_sku} ${JSON.stringify(res.err).slice(0,140)}`); }
       if (res.stop) { console.log(`\n⛔ DAILY_VARIANT_LIMIT reached — stopping (resets in ~24h). created ${created} this run.`); hitCap=true; break; }
@@ -786,7 +845,7 @@ if (require.main === module) (async () => {
   // activatedCount counts every product that went ACTIVE (incl. sample-only, whose untracked
   // sample returns no inventory skus — so it can't be derived from activatedSkus.length/2).
   const activatedN = activatedCount;
-  console.log(`\n\n${COMMIT?`CREATED ${created} (${ACTIVATE?activatedN+' ACTIVE / '+(created-activatedN)+' DRAFT':'all DRAFT'}), linked-existing ${linked} (dedup guard), failed ${failed}`:`DRY-RUN planned ${plan.length} products${ACTIVATE?` (${plan.filter(p=>p.willActivate).length} would activate)`:''}`} → ${planFile}`);
+  console.log(`\n\n${COMMIT?`CREATED ${created} (${ACTIVATE?activatedN+' ACTIVE / '+(created-activatedN)+' DRAFT':'all DRAFT'}), linked-existing ${linked} (dedup guard), held ${held} (banned-Brand denylist), failed ${failed}`:`DRY-RUN planned ${plan.length} products${ACTIVATE?` (${plan.filter(p=>p.willActivate).length} would activate)`:''}${plan.some(p=>p.held)?` — ${plan.filter(p=>p.held).length} HELD (banned-Brand denylist)`:''}`} → ${planFile}`);
   if (COMMIT) console.log(`restore map: ${RESTORE_FILE} (batch ${BATCH_ID})`);
   if (!COMMIT) {
     console.log('Re-run with --commit to create these on Shopify (DRAFT status, dw_unified logged first).');

← 39f8c8fe China Seas mailer: 'Shop by Theme' → 'Design Idea Spotlight'  ·  back to Designer Wallcoverings  ·  China Seas mailer: repoint Design Idea Spotlight pills — Nat 9a95b5b4 →