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