[object Object]

← back to Designer Wallcoverings

cadence: DWBR per-product house-line emission (Malibu Wallpaper/LA Walls), gated OFF by default

115f2e5b5655c4e953c85ff5a58a3f015a71d29d · 2026-06-25 10:41:36 -0700 · Steve Abrams

Brewster & York map key stays (ties to vendor_registry discount gate + brewster_catalog),
but emitted product vendor + global.Brand now resolve from a per-product house_line DB column
(houseLineCol) with fallback Malibu Wallpaper — never the banned 'Brewster & York' key.
Gated behind DWBR_HOUSE_LINE_REMAP=1 / --house-line (default OFF) so committing is inert on the
live Mac2 cadence: when OFF, brewster keeps emitting the key and is held by bannedBrandReason().
Verified: 3 queued items (DWBR-170697/698/699) -> held when OFF, publish as Malibu Wallpaper when ON.

Files touched

Diff

commit 115f2e5b5655c4e953c85ff5a58a3f015a71d29d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jun 25 10:41:36 2026 -0700

    cadence: DWBR per-product house-line emission (Malibu Wallpaper/LA Walls), gated OFF by default
    
    Brewster & York map key stays (ties to vendor_registry discount gate + brewster_catalog),
    but emitted product vendor + global.Brand now resolve from a per-product house_line DB column
    (houseLineCol) with fallback Malibu Wallpaper — never the banned 'Brewster & York' key.
    Gated behind DWBR_HOUSE_LINE_REMAP=1 / --house-line (default OFF) so committing is inert on the
    live Mac2 cadence: when OFF, brewster keeps emitting the key and is held by bannedBrandReason().
    Verified: 3 queued items (DWBR-170697/698/699) -> held when OFF, publish as Malibu Wallpaper when ON.
---
 shopify/scripts/cadence/cadence-import.js | 48 ++++++++++++++++++++++++++++---
 shopify/scripts/cadence/vendors.js        | 16 ++++++++++-
 2 files changed, 59 insertions(+), 5 deletions(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 59e7e353..2e43172d 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -51,6 +51,15 @@ const ADD_TO_COLLECTIONS = flag('--collections');       // GATED: add newly-publ
 // runner won't create any sample-only product until the env/flag is enabled. The few-SKU manual
 // test bypasses the gate via --sampleonly (or --only <SampleOnlyVendor>, which whitelists it).
 const SAMPLEONLY_DRIP = flag('--sampleonly') || process.env.QUADRILLE_SAMPLEONLY_DRIP === '1';
+// DWBR house-line remap (Steve 2-brand rule, 2026-06-25). OFF by default — when OFF, vendors that
+// declare a houseLineCol (today only 'Brewster & York') keep emitting the map key as the product
+// vendor/Brand and are HELD by bannedBrandReason() (nothing publishes), so committing the remap
+// code does NOT change live behavior. When ON (env DWBR_HOUSE_LINE_REMAP=1 or --house-line), the
+// emitted vendor + global.Brand come from each product's resolved house_line (DB column) with the
+// config's houseLineFallback ('Malibu Wallpaper') for any blank row — NEVER 'Brewster & York'.
+// Flip ON only AFTER Steve approves + the gated house_line backfill SQL has run. (Mirrors the
+// QUADRILLE_SAMPLEONLY_DRIP / --collections default-OFF gating precedent in this file.)
+const DWBR_HOUSE_LINE = flag('--house-line') || process.env.DWBR_HOUSE_LINE_REMAP === '1';
 const TODAY = new Date().toISOString().slice(0, 10);
 // Defensive vendor denylist — even if a vendor is in vendors.js + discount_confirmed, NEVER
 // import these (belt-and-suspenders for sub-brand/registry drift). Today vendors.js already
@@ -350,6 +359,19 @@ function selectSkus(cfg, limit) {
     hasAllImg = (psql(`SELECT 1 FROM information_schema.columns WHERE table_name='${t}' AND column_name='${c}' LIMIT 1;`) || '').trim() === '1';
   } catch { hasAllImg = false; }
   const allImgSel = hasAllImg ? `, (${ALLIMG})::text AS all_images` : '';
+  // Per-product front-facing HOUSE LINE column (e.g. DWBR brewster_catalog.house_line → Malibu
+  // Wallpaper / LA Walls). Selected only when the vendor opts in (cfg.houseLineCol), the remap flag
+  // is ON, AND the column exists (populated by the GATED house_line backfill). When absent/blank,
+  // emitVendorFor() uses cfg.houseLineFallback. Probe mirrors the all_images existence check.
+  let hasHouseLine = false;
+  if (DWBR_HOUSE_LINE && cfg.houseLineCol) {
+    try {
+      const t = String(cfg.table).replace(/[^a-z0-9_]/gi,'');
+      const c = String(cfg.houseLineCol).replace(/[^a-z0-9_]/gi,'');
+      hasHouseLine = (psql(`SELECT 1 FROM information_schema.columns WHERE table_name='${t}' AND column_name='${c}' LIMIT 1;`) || '').trim() === '1';
+    } catch { hasHouseLine = false; }
+  }
+  const houseLineSel = hasHouseLine ? `, (${String(cfg.houseLineCol).replace(/[^a-z0-9_]/gi,'')})::text AS house_line` : '';
   // sampleOnly (Quadrille-house, no cost): select net-new rows scoped by brand, NO cost column,
   // NO cost>0 filter. description pulled from ai_description / description for the activation gate.
   if (cfg.sampleOnly) {
@@ -358,22 +380,22 @@ function selectSkus(cfg, limit) {
     // (free-text, enrichment ongoing) would corrupt the row parser. Collapse those control chars to a
     // space AT THE SQL LEVEL so the tab/newline split stays sound regardless of description content.
     const descCol = `regexp_replace((${rawDescCol})::text, '[\\t\\n\\r]+', ' ', 'g')`;
-    const sql = `SELECT ${cols}, (${descCol})::text AS vendor_desc${allImgSel}
+    const sql = `SELECT ${cols}, (${descCol})::text AS vendor_desc${allImgSel}${houseLineSel}
       FROM ${cfg.table}
       WHERE coalesce(shopify_product_id::text,'')=''${brandFilterClause(cfg)}${dedupSkipClause(cfg.table)}
       ORDER BY dw_sku LIMIT ${limit};`;
     const out = psql(sql);
     if (!out) return [];
-    const keys = [...STD, 'vendor_desc', ...(hasAllImg ? ['all_images'] : [])];
+    const keys = [...STD, 'vendor_desc', ...(hasAllImg ? ['all_images'] : []), ...(hasHouseLine ? ['house_line'] : [])];
     return out.split('\n').map(line => { const v=line.split('\t'); const o={}; keys.forEach((k,i)=>o[k]=v[i]); o.material=mfVal(o.material); o.cost=0; o.sampleOnly=true; return o; });
   }
-  const sql = `SELECT ${cols}, (${cfg.costExpr})::numeric AS cost${sellSel}${allImgSel}
+  const sql = `SELECT ${cols}, (${cfg.costExpr})::numeric AS cost${sellSel}${allImgSel}${houseLineSel}
     FROM ${cfg.table}
     WHERE coalesce(shopify_product_id::text,'')='' AND (${cfg.costExpr})::numeric > 0${dedupSkipClause(cfg.table)}
     ORDER BY dw_sku LIMIT ${limit};`;
   const out = psql(sql);
   if (!out) return [];
-  const keys = [...STD, 'cost', ...(cfg.sellExpr ? ['sell'] : []), ...(hasAllImg ? ['all_images'] : [])];
+  const keys = [...STD, 'cost', ...(cfg.sellExpr ? ['sell'] : []), ...(hasAllImg ? ['all_images'] : []), ...(hasHouseLine ? ['house_line'] : [])];
   return out.split('\n').map(line => { const v=line.split('\t'); const o={}; keys.forEach((k,i)=>o[k]=v[i]); o.material=mfVal(o.material); o.cost=parseFloat(o.cost); if(cfg.sellExpr) o.sell=parseFloat(o.sell); return o; });
 }
 
@@ -469,8 +491,26 @@ function buildSampleOnlyInput(vendor, cfg, row, activate) {
   return { input, title, retail: 0, willActivate, ready, sampleOnly: true };
 }
 
+// ---- resolve the FRONT-FACING vendor/Brand label for a product ----
+// For most vendors this is just the VENDORS-map key. For vendors that opt into a per-product
+// HOUSE LINE (cfg.houseLineCol — today only 'Brewster & York', whose key is a BANNED front-facing
+// trade-parent name), the emitted vendor + global.Brand come from the product's resolved house_line
+// DB column, falling back to cfg.houseLineFallback ('Malibu Wallpaper') for any blank row — and
+// NEVER the banned key. Gated behind DWBR_HOUSE_LINE so committing this code is inert until Steve
+// flips it on (after approving + running the house_line backfill). When OFF, returns the key
+// unchanged → bannedBrandReason() holds the product → nothing publishes (status quo).
+function emitVendorFor(vendor, cfg, row) {
+  if (DWBR_HOUSE_LINE && cfg && cfg.houseLineCol) {
+    const hl = String((row && row[cfg.houseLineCol]) || '').trim();
+    if (hl) return hl;
+    if (cfg.houseLineFallback) return cfg.houseLineFallback;
+  }
+  return vendor;
+}
+
 // ---- build the Shopify productSet input (generic across standard-schema vendors) ----
 function buildInput(vendor, cfg, row, retail, activate) {
+  vendor = emitVendorFor(vendor, cfg, row);   // per-product house line (DWBR → Malibu Wallpaper / LA Walls); never the banned key
   if (cfg.sampleOnly) return buildSampleOnlyInput(vendor, cfg, row, activate);
   const stripCodes = s => String(s||'').replace(/\(\s*[A-Za-z]?\d{3,}[^)]*\)/g, '').replace(/\s{2,}/g,' ').trim(); // kill (T457) etc
   const pattern = titleCase(stripCodes(row.pattern_name || ''));
diff --git a/shopify/scripts/cadence/vendors.js b/shopify/scripts/cadence/vendors.js
index 3c3d304f..077e67cf 100644
--- a/shopify/scripts/cadence/vendors.js
+++ b/shopify/scripts/cadence/vendors.js
@@ -43,7 +43,21 @@ module.exports = {
   // cost = wholesale (populated on 8697/8715 rows), us_map = MAP, price_retail = MSRP.
   // discount_confirmed=true. retail = cost/0.65/0.85 (formula sits above us_map on samples,
   // so MAP floor is satisfied without a sellExpr). Steve 2026-06-12 "you have the price lists".
-  'Brewster & York': { table: 'brewster_catalog',    costExpr: 'cost',                 soldBy: 'Single Roll', productType: 'Wallcovering', discountNote: 'cost from downloaded York/Brewster price list (wholesale); us_map available as MAP floor' },
+  //
+  // FRONT-FACING NAME: "Brewster & York" is a BANNED front-facing trade-parent name (Steve
+  // "!!!" 2026-06-23). The map key STAYS 'Brewster & York' (it ties to vendor_registry's
+  // discount gate + this catalog table) but the EMITTED product vendor + global.Brand must be a
+  // per-product HOUSE LINE, never the key. Per Steve's 2-brand rule the DWBR line collapses to
+  //   Malibu Wallpaper  +  LA Walls   (former "Malibu Walls" is MERGED into "Malibu Wallpaper").
+  // houseLineCol  = the brewster_catalog column carrying the resolved line per product (populated
+  //                 by the GATED house_line backfill SQL — drafted, not yet run).
+  // houseLineFallback = used for any row whose house_line is NULL/blank (the rule's global fallback).
+  // EMISSION IS OFF BY DEFAULT — gated behind DWBR_HOUSE_LINE_REMAP=1 / --house-line in
+  // cadence-import.js, so committing this config does NOT change live behavior. Until Steve flips
+  // the flag ON (after approving + running the backfill), brewster products keep emitting the key
+  // and are HELD by the bannedBrandReason() guard (nothing publishes). 'Brewster & York' can never
+  // reach Shopify in EITHER state.
+  'Brewster & York': { table: 'brewster_catalog',    costExpr: 'cost',                 soldBy: 'Single Roll', productType: 'Wallcovering', houseLineCol: 'house_line', houseLineFallback: 'Malibu Wallpaper', discountNote: 'cost from downloaded York/Brewster price list (wholesale); us_map available as MAP floor' },
   // Kravet: cost loaded from authoritative GDrive wholesale list (Kravet Price list 1-20-26.xlsx,
   // eff 2026-01-17) into cost_price = NEW WHLS incl tariff; sell floored at NEW MAP (Steve 2026-06-11).
   // Kravet + ALL subbrands (Brunschwig, Lee Jofa, GP&J Baker, Cole & Son, Donghia,

← 163fa1fb Anna French Phase 1: install 789 dormant 301 redirects on li  ·  back to Designer Wallcoverings  ·  cadence: add Anna French config with per-row product type (A e4435faa →