← 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
M shopify/scripts/cadence/cadence-import.js
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 →