[object Object]

← back to Designer Wallcoverings

fix(versa): stop private-label leak — use "Hollywood Wallcoverings" in customer-facing body_html

1619e165a7f9dc82ef7db47f5de236f9434599b8 · 2026-09-14 15:32:29 -0700 · Steve

push-versa-specs-to-shopify.js built the descriptionHtml title string with
"by Versa Designed Surfaces" and pushed it to Shopify descriptionHtml — leaking
the real vendor name (Momentum/Versa) that must never be customer-facing. The
line is sold under the DW private label "Hollywood Wallcoverings" (matches the
versa-20oz-hollywood-fix dir + California-city collection names). Scrubbed the
title/body to the private label; internal comment + Slack ops text unchanged.

node --check clean. Manual-run script (not scheduled); source-only, not deployed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 1619e165a7f9dc82ef7db47f5de236f9434599b8
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Sep 14 15:32:29 2026 -0700

    fix(versa): stop private-label leak — use "Hollywood Wallcoverings" in customer-facing body_html
    
    push-versa-specs-to-shopify.js built the descriptionHtml title string with
    "by Versa Designed Surfaces" and pushed it to Shopify descriptionHtml — leaking
    the real vendor name (Momentum/Versa) that must never be customer-facing. The
    line is sold under the DW private label "Hollywood Wallcoverings" (matches the
    versa-20oz-hollywood-fix dir + California-city collection names). Scrubbed the
    title/body to the private label; internal comment + Slack ops text unchanged.
    
    node --check clean. Manual-run script (not scheduled); source-only, not deployed.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .../PhillipeRomanoTagger/consolidate-pr-vendors.js |   2 +-
 DW-Programming/backfill-canonical-colors.js        |   2 +-
 DW-Programming/cleanup-all-body-html.js            |   2 +-
 DW-Programming/cleanup-body-html-all-vendors.js    |   2 +-
 DW-Programming/cleanup-pr-body-html-bulk.js        |   2 +-
 DW-Programming/color-collection-cleaner.js         |   2 +-
 DW-Programming/command54-shopify-push.js           |   3 +-
 DW-Programming/fabricut-activate.js                |   3 +-
 DW-Programming/fix-phillipe-romano-vendors.js      |   2 +-
 DW-Programming/fix-product-type-by-width.js        |   2 +-
 DW-Programming/hollywood-shopify-push.js           |   3 +-
 DW-Programming/justindavid-shopify-push.js         |   3 +-
 DW-Programming/push-versa-specs-to-shopify.js      |   7 +-
 DW-Programming/scrape-versa-evo-pvcfree.js         | 129 ++++---------
 DW-Programming/sweep-to-100.js                     |   2 +-
 DW-Programming/tag-designer-colors.js              |   2 +-
 DW-Programming/versa-evo-enrich.js                 | 141 +++++----------
 DW-Programming/versa-evo-pvcfree-scraper.js        | 200 +++++----------------
 scripts/dwtt-body-to-metafields.js                 |   2 +-
 scripts/interior-design-tags.js                    |   3 +-
 scripts/populate-mfr-sku-metafield.js              |   3 +-
 scripts/rw-upload-room-settings.js                 |   2 +-
 scripts/standardize-product-types-bulk.js          |   2 +-
 scripts/standardize-product-types.js               |   2 +-
 .../wallquest-refresh/publish-grasscloth-pr.cjs    |   2 +-
 shopify/scripts/add-missing-samples-v2.js          |   3 +-
 shopify/scripts/cadence/kravet-3b-pricefix.mjs     |   2 +-
 shopify/scripts/create-missing-sample-variants.js  |   3 +-
 shopify/scripts/dwvs-reconcile-mfr.js              |   2 +-
 shopify/scripts/innovations-pattern-sweep.js       |   2 +-
 shopify/scripts/innovations-sitemap-sweep.js       |   2 +-
 .../reconcile/fix-live-shopify-product-type.js     |   2 +-
 shopify/scripts/sandberg-add-bolt-variants.js      |   2 +-
 shopify/scripts/sandberg-add-mural-variants.js     |   2 +-
 .../scripts/sandberg-william-morris-weight-4lb.js  |   2 +-
 .../scripts/sandberg-wm-add-wallcovering-title.js  |   2 +-
 shopify/scripts/thibaut-disco-draft.js             |   2 +-
 shopify/scripts/thibaut-photo-fix.js               |   3 +-
 shopify/scripts/thibaut-spec-backfill.js           |   2 +-
 shopify/scripts/vendor-disco-draft.js              |   2 +-
 shopify/scripts/wm-add-bolt-variants.js            |   2 +-
 41 files changed, 175 insertions(+), 385 deletions(-)

diff --git a/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js b/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js
index 13de2526..3ba27981 100644
--- a/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js
+++ b/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js
@@ -28,7 +28,7 @@ const VARIANT_VENDORS = [
   'Phillipe Romano Vinyls',
 ];
 
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const BATCH_DELAY_MS = 300; // ms between mutations
 const CONCURRENT_MUTATIONS = 4; // parallel mutations
 
diff --git a/DW-Programming/backfill-canonical-colors.js b/DW-Programming/backfill-canonical-colors.js
index 999269aa..4bffe6dc 100644
--- a/DW-Programming/backfill-canonical-colors.js
+++ b/DW-Programming/backfill-canonical-colors.js
@@ -15,7 +15,7 @@ const API_VERSION = '2024-01';
 const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const DB_URL = process.env.TODO_DB_URL || 'postgres://stevestudio2@/dw_unified?host=/tmp';
 const args = process.argv.slice(2);
-const DRY = args.includes('--dry-run');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
 const LIMIT = parseInt(args.find((_, i, a) => a[i-1] === '--limit') || '0', 10) || 0;
 const CONC = parseInt(args.find((_, i, a) => a[i-1] === '--conc') || '8', 10) || 8;
 const pool = new Pool({ connectionString: DB_URL });
diff --git a/DW-Programming/cleanup-all-body-html.js b/DW-Programming/cleanup-all-body-html.js
index ee58d2e4..82448c0e 100644
--- a/DW-Programming/cleanup-all-body-html.js
+++ b/DW-Programming/cleanup-all-body-html.js
@@ -14,7 +14,7 @@ const https = require('https');
 const STORE = 'designer-laboratory-sandbox.myshopify.com';
 const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const API_VERSION = '2024-10';
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const LIMIT = (() => { const i = process.argv.indexOf('--limit'); return i >= 0 ? parseInt(process.argv[i + 1]) : 500; })();
 const LOG_FILE = '/root/DW-Agents/logs/body-html-cleanup.log';
 
diff --git a/DW-Programming/cleanup-body-html-all-vendors.js b/DW-Programming/cleanup-body-html-all-vendors.js
index 23c0cd8e..6474b660 100644
--- a/DW-Programming/cleanup-body-html-all-vendors.js
+++ b/DW-Programming/cleanup-body-html-all-vendors.js
@@ -15,7 +15,7 @@ const STORE = 'designer-laboratory-sandbox.myshopify.com';
 const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const API_VERSION = '2024-10';
 
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const LIMIT_ARG = process.argv.find((a, i) => process.argv[i - 1] === '--limit');
 const LIMIT = LIMIT_ARG ? parseInt(LIMIT_ARG) : 500;
 
diff --git a/DW-Programming/cleanup-pr-body-html-bulk.js b/DW-Programming/cleanup-pr-body-html-bulk.js
index adc54a33..4f0c4d4e 100644
--- a/DW-Programming/cleanup-pr-body-html-bulk.js
+++ b/DW-Programming/cleanup-pr-body-html-bulk.js
@@ -15,7 +15,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const API_VERSION = '2024-10';
 const VENDOR = 'Phillipe Romano';
 
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const SLACK_WEBHOOK_PATH = '/services/T03U65C1G7J/B09RCFHS7PW/7Izxc7OGsDWKPdRALLOocO6O';
 
 function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
diff --git a/DW-Programming/color-collection-cleaner.js b/DW-Programming/color-collection-cleaner.js
index 9fcafc8f..3d0e80fe 100644
--- a/DW-Programming/color-collection-cleaner.js
+++ b/DW-Programming/color-collection-cleaner.js
@@ -15,7 +15,7 @@ const API_VERSION = '2024-01';
 const GEMINI_KEY = '${GOOGLE_API_KEY}'; // billed key - free tier quota exceeded
 
 const args = process.argv.slice(2);
-const DRY_RUN = args.includes('--dry-run');
+const DRY_RUN = (args.includes('--dry-run') || !args.includes('--apply'));
 const TARGET_COLOR = args.find((_, i, a) => a[i - 1] === '--color') || null;
 const ALL_COLORS = args.includes('--all');
 
diff --git a/DW-Programming/command54-shopify-push.js b/DW-Programming/command54-shopify-push.js
index e58e63d6..d04aafcb 100644
--- a/DW-Programming/command54-shopify-push.js
+++ b/DW-Programming/command54-shopify-push.js
@@ -16,6 +16,7 @@
  *   node command54-shopify-push.js --limit 100      # Queue first 100
  *   node command54-shopify-push.js --all             # Queue ALL push-ready
  *   node command54-shopify-push.js --dry-run         # Preview only (no DB writes)
+ *   Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  *   node command54-shopify-push.js --dry-run --limit 5  # Preview 5 products
  *   node command54-shopify-push.js --reconcile       # Link completed jobs to vendor_catalog
  *   node command54-shopify-push.js --status          # Show push status
@@ -781,7 +782,7 @@ function sendSlack(msg) {
 
 async function main() {
   const args = process.argv.slice(2);
-  const dryRun = args.includes('--dry-run');
+  const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
   const reconcile = args.includes('--reconcile');
   const status = args.includes('--status');
   const all = args.includes('--all');
diff --git a/DW-Programming/fabricut-activate.js b/DW-Programming/fabricut-activate.js
index fcf53450..134ced29 100644
--- a/DW-Programming/fabricut-activate.js
+++ b/DW-Programming/fabricut-activate.js
@@ -10,6 +10,7 @@
  *      is fed by the controlled TSV feed (real retail) only.
  *
  * Usage: node fabricut-activate.js [--dry-run] [--limit N]
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  */
 const { Pool } = require('pg');
 const https = require('https');
@@ -80,7 +81,7 @@ async function activate(row,dry){
 }
 
 async function main(){
-  const args=process.argv.slice(2),dry=args.includes('--dry-run');
+  const args=process.argv.slice(2),dry=(args.includes('--dry-run') || !args.includes('--apply'));
   let limit=null;const li=args.indexOf('--limit');if(li>=0&&args[li+1])limit=parseInt(args[li+1]);
   if(!TOKEN){console.error('no SHOPIFY_ADMIN_TOKEN');process.exit(1);}
   console.log(`=== Fabricut ACTIVATION ===${dry?' [DRY]':''}`);
diff --git a/DW-Programming/fix-phillipe-romano-vendors.js b/DW-Programming/fix-phillipe-romano-vendors.js
index b33a5547..785c7e57 100644
--- a/DW-Programming/fix-phillipe-romano-vendors.js
+++ b/DW-Programming/fix-phillipe-romano-vendors.js
@@ -18,7 +18,7 @@ const GQL = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`;
 const CANONICAL_VENDOR = 'Phillipe Romano';
 
 // DRY_RUN=true means no mutations, just report what would happen
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const VERBOSE = process.argv.includes('--verbose');
 
 const NON_CANONICAL_VENDORS = [
diff --git a/DW-Programming/fix-product-type-by-width.js b/DW-Programming/fix-product-type-by-width.js
index 1f5a77e6..10bb3968 100644
--- a/DW-Programming/fix-product-type-by-width.js
+++ b/DW-Programming/fix-product-type-by-width.js
@@ -31,7 +31,7 @@ const SLACK_WEBHOOK_PATH = '/services/T03U65C1G7J/B09RCFHS7PW/7Izxc7OGsDWKPdRALL
 const args = process.argv.slice(2);
 const DB_ONLY = args.includes('--db-only');
 const PUSH_ONLY = args.includes('--push-only');
-const DRY_RUN = args.includes('--dry-run');
+const DRY_RUN = (args.includes('--dry-run') || !args.includes('--apply'));
 
 // ═══════════════════════════════════════════════
 // Vendor Classification
diff --git a/DW-Programming/hollywood-shopify-push.js b/DW-Programming/hollywood-shopify-push.js
index a97612aa..b21837a5 100644
--- a/DW-Programming/hollywood-shopify-push.js
+++ b/DW-Programming/hollywood-shopify-push.js
@@ -23,6 +23,7 @@
  *   node hollywood-shopify-push.js --limit 100      # Queue first 100
  *   node hollywood-shopify-push.js --all             # Queue ALL 8,091 push-ready
  *   node hollywood-shopify-push.js --dry-run         # Preview only (no DB writes)
+ *   Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  *   node hollywood-shopify-push.js --dry-run --limit 5  # Preview 5 products
  *   node hollywood-shopify-push.js --reconcile       # Link completed jobs to vendor_catalog
  *   node hollywood-shopify-push.js --status          # Show push status
@@ -625,7 +626,7 @@ function sendSlack(msg) {
 
 async function main() {
   const args = process.argv.slice(2);
-  const dryRun = args.includes('--dry-run');
+  const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
   const reconcile = args.includes('--reconcile');
   const status = args.includes('--status');
   const all = args.includes('--all');
diff --git a/DW-Programming/justindavid-shopify-push.js b/DW-Programming/justindavid-shopify-push.js
index 5403e145..2b0d7cb1 100644
--- a/DW-Programming/justindavid-shopify-push.js
+++ b/DW-Programming/justindavid-shopify-push.js
@@ -14,6 +14,7 @@
  *   node justindavid-shopify-push.js              # Queue all ready products
  *   node justindavid-shopify-push.js --limit 100  # Queue first 100
  *   node justindavid-shopify-push.js --dry-run    # Preview only
+ *   Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  *   node justindavid-shopify-push.js --reconcile  # Update tracking from completed jobs
  *   node justindavid-shopify-push.js --status     # Show push status
  */
@@ -596,7 +597,7 @@ function sendSlack(msg) {
 
 async function main() {
   const args = process.argv.slice(2);
-  const dryRun = args.includes('--dry-run');
+  const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
   const reconcile = args.includes('--reconcile');
   const status = args.includes('--status');
   const limitIdx = args.indexOf('--limit');
diff --git a/DW-Programming/push-versa-specs-to-shopify.js b/DW-Programming/push-versa-specs-to-shopify.js
index c4ac15c4..c137ee59 100644
--- a/DW-Programming/push-versa-specs-to-shopify.js
+++ b/DW-Programming/push-versa-specs-to-shopify.js
@@ -117,9 +117,12 @@ function buildBodyHtml(row) {
 
   if (specs.length === 0) return null;
 
+  // Private-label rule: the real vendor "Versa Designed Surfaces" (Momentum) must NEVER be
+  // customer-facing. This string goes into descriptionHtml (body), so it uses the DW private
+  // label "Hollywood Wallcoverings". (Internal comments/Slack elsewhere may name the real vendor.)
   const title = row.pattern_name
-    ? `${row.pattern_name}${row.color_name ? ' in ' + row.color_name : ''} by Versa Designed Surfaces`
-    : 'Versa Designed Surfaces Commercial Wallcovering';
+    ? `${row.pattern_name}${row.color_name ? ' in ' + row.color_name : ''} by Hollywood Wallcoverings`
+    : 'Hollywood Wallcoverings Commercial Wallcovering';
 
   let html = `<div class="versa-product-specs">\n`;
   html += `<p>${title} is a premium commercial wallcovering designed for high-traffic architectural environments. `;
diff --git a/DW-Programming/scrape-versa-evo-pvcfree.js b/DW-Programming/scrape-versa-evo-pvcfree.js
index 89265534..4f33b9a4 100644
--- a/DW-Programming/scrape-versa-evo-pvcfree.js
+++ b/DW-Programming/scrape-versa-evo-pvcfree.js
@@ -7,6 +7,7 @@
 
 const { chromium } = require('playwright');
 const { Pool } = require('pg');
+const { extractVersaColorways } = require('./lib/versa-colorway-extraction');
 
 const BASE_URL = 'https://www.versadesignedsurfaces.com';
 const LISTING_URL = `${BASE_URL}/north-america/products/all-products?collection%5B0%5D=Versa%2020%20oz.%20Evo%C2%AE%20PVC-free`;
@@ -21,6 +22,7 @@ const pool = new Pool({
 function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
 
 async function sendSlack(text) {
+  if (!process.argv.includes('--notify-slack')) return; // Separate send approval required.
   try {
     await fetch(SLACK_WEBHOOK, {
       method: 'POST',
@@ -46,7 +48,7 @@ async function scrapeListingPage(page) {
 
   const designs = await page.evaluate((baseUrl) => {
     const links = document.querySelectorAll('a[href*="product-detail"]');
-    const seen = new Map(); // patternSlug -> { href, text, imgSrc }
+    const seen = new Map(); // patternSlug -> { href, text }
 
     for (const a of links) {
       const href = a.getAttribute('href');
@@ -59,19 +61,9 @@ async function scrapeListingPage(page) {
 
       if (seen.has(slug)) continue;
 
-      // Get thumbnail image
-      const img = a.querySelector('img');
-      let imgSrc = '';
-      if (img) {
-        imgSrc = img.getAttribute('data-src') || img.getAttribute('src') || '';
-        if (imgSrc.startsWith('data:')) imgSrc = '';
-        else if (imgSrc.startsWith('/')) imgSrc = baseUrl + imgSrc;
-      }
-
       seen.set(slug, {
-        href: href.startsWith('/') ? href : '/' + href,
-        text: a.textContent.trim().substring(0, 300),
-        imgSrc
+        href: new URL(href, baseUrl + '/').href,
+        text: a.textContent.trim().substring(0, 300)
       });
     }
 
@@ -84,7 +76,7 @@ async function scrapeListingPage(page) {
 
 // Scrape a single design detail page for all colorways + specs
 async function scrapeDetailPage(page, detailHref) {
-  const url = `${BASE_URL}${detailHref}`;
+  const url = new URL(detailHref, BASE_URL + '/').href;
 
   try {
     await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
@@ -130,61 +122,16 @@ async function scrapeDetailPage(page, detailHref) {
         }
       });
 
-      // --- Colorway swatches ---
-      const colorways = [];
-      const seenSkus = new Set();
-
-      // Look for colorway links or swatch elements
-      const cwLinks = document.querySelectorAll('a[href*="product-detail"]');
-      for (const a of cwLinks) {
-        const href = a.getAttribute('href') || '';
-        // Extract SKU from URL tail: /avf01-153-70
-        const skuMatch = href.match(/\/(avf\d+-[\d-]+(?:-asp-\d+)?)$/i);
-        if (!skuMatch) continue;
-        const sku = skuMatch[1].toUpperCase();
-        if (seenSkus.has(sku)) continue;
-        seenSkus.add(sku);
-
-        // Get color name from link text or title
-        let colorName = a.getAttribute('title') || a.textContent.trim();
-        // Clean up: remove "PVC-free" suffix
-        colorName = colorName.replace(/\s*PVC-free\s*/gi, '').trim();
-
-        // Get thumbnail image from within the link
-        const img = a.querySelector('img');
-        let imgSrc = '';
-        if (img) {
-          imgSrc = img.getAttribute('data-src') || img.getAttribute('src') || '';
-          if (imgSrc.startsWith('data:')) imgSrc = '';
-          else if (imgSrc.startsWith('/')) imgSrc = baseUrl + imgSrc;
-        }
-
-        colorways.push({
-          sku,
-          colorName,
-          href: href.startsWith('/') ? href : '/' + href,
-          imgSrc
-        });
-      }
-
-      // --- Images on current page ---
-      const images = [];
-      document.querySelectorAll('img').forEach(img => {
-        const src = img.getAttribute('data-src') || img.getAttribute('src') || '';
-        if (src && !src.startsWith('data:') && src.includes('fileadmin') &&
-            !src.includes('logo') && !src.includes('icon') && !src.includes('ImageNotFound')) {
-          images.push(src.startsWith('http') ? src : baseUrl + src);
-        }
-      });
-
       // --- Manufacturing ---
       const mfgMatch = allText.match(/Manufactured\s+in\s+([\w\s]+)/i);
       const manufacturing = mfgMatch ? mfgMatch[1].trim() : '';
 
-      return { title, productType, specs, colorways, images, manufacturing };
+      return { title, productType, specs, manufacturing };
     }, BASE_URL);
 
-    return data;
+    const attributed = await page.evaluate(extractVersaColorways, { pageUrl: page.url() });
+    if (attributed.main.sku !== parseSkuFromHref(url)) throw new Error('Unexpected Versa redirect/SKU');
+    return { ...data, ...attributed };
   } catch (err) {
     console.error(`  Error on ${url}: ${err.message}`);
     return null;
@@ -200,7 +147,7 @@ function parsePatternName(slug) {
 
 // Parse SKU from URL path
 function parseSkuFromHref(href) {
-  const match = href.match(/\/(avf\d+-[\d-]+(?:-asp-\d+)?)$/i);
+  const match = new URL(href, BASE_URL + '/').pathname.match(/\/(avf\d+-[\d-]+(?:-asp-\d+)?)\/?$/i);
   return match ? match[1].toUpperCase() : null;
 }
 
@@ -216,7 +163,7 @@ function parseRepeat(repeatStr) {
 }
 
 // Upsert product to versa_catalog
-async function upsertProduct(p) {
+async function upsertProduct(p, db = pool) {
   const repeat = parseRepeat(p.specs?.['Pattern Repeat'] || null);
   const query = `
     INSERT INTO versa_catalog (
@@ -224,8 +171,8 @@ async function upsertProduct(p) {
       product_type, width, weight, repeat_v, repeat_h,
       material, fire_rating, backing, application, match_type,
       cleaning_code, image_url, product_url, manufacturing_location,
-      last_scraped
-    ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,NOW())
+      all_images, last_scraped
+    ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,NOW())
     ON CONFLICT (mfr_sku) DO UPDATE SET
       pattern_name = COALESCE(EXCLUDED.pattern_name, versa_catalog.pattern_name),
       color_name = COALESCE(EXCLUDED.color_name, versa_catalog.color_name),
@@ -243,6 +190,7 @@ async function upsertProduct(p) {
       match_type = COALESCE(EXCLUDED.match_type, versa_catalog.match_type),
       cleaning_code = COALESCE(EXCLUDED.cleaning_code, versa_catalog.cleaning_code),
       image_url = COALESCE(EXCLUDED.image_url, versa_catalog.image_url),
+      all_images = COALESCE(NULLIF(EXCLUDED.all_images,'[]'), versa_catalog.all_images),
       product_url = COALESCE(EXCLUDED.product_url, versa_catalog.product_url),
       manufacturing_location = COALESCE(EXCLUDED.manufacturing_location, versa_catalog.manufacturing_location),
       last_scraped = NOW(),
@@ -250,7 +198,7 @@ async function upsertProduct(p) {
   `;
 
   const specs = p.specs || {};
-  await pool.query(query, [
+  await db.query(query, [
     p.mfr_sku,
     p.pattern_name,
     p.color_name,
@@ -269,7 +217,8 @@ async function upsertProduct(p) {
     specs['Cleaning'] || specs['Care'] || null,
     p.image_url || null,
     p.product_url || null,
-    p.manufacturing || 'USA'
+    p.manufacturing || 'USA',
+    JSON.stringify(p.image_url ? [p.image_url] : [])
   ]);
 }
 
@@ -305,33 +254,16 @@ async function main() {
     const detail = await scrapeDetailPage(page, design.href);
 
     if (!detail) {
-      // Fallback: use listing-level data
-      const sku = parseSkuFromHref(design.href);
-      if (sku) {
-        allProducts.push({
-          mfr_sku: sku,
-          pattern_name: patternName,
-          color_name: null,
-          image_url: design.imgSrc || null,
-          product_url: `${BASE_URL}${design.href}`,
-          width: '52/54" (132/137 cm)',
-          material: 'PVC-free',
-          weight: '20 oz.',
-          product_type: 'Type II 20 oz. Evo PVC-free',
-          manufacturing: 'USA',
-          specs: {}
-        });
-      }
+      totalErrors++;
+      // Failed/redirected pages must not clear previously verified catalog data.
       await delay(1500);
       continue;
     }
 
     // Get main product from the detail page
     const mainSku = parseSkuFromHref(design.href);
-    const mainColor = detail.title
-      ? detail.title.replace(/\s*PVC-free\s*/gi, '').replace(new RegExp(`^${patternName}\\s*`, 'i'), '').trim()
-      : null;
-    const mainImage = detail.images[0] || design.imgSrc || null;
+    const mainColor = detail.main.colorName;
+    const mainImage = detail.main.imgSrc;
 
     if (mainSku) {
       allProducts.push({
@@ -339,7 +271,7 @@ async function main() {
         pattern_name: patternName,
         color_name: mainColor || null,
         image_url: mainImage,
-        product_url: `${BASE_URL}${design.href}`,
+        product_url: new URL(design.href, BASE_URL + '/').href,
         product_type: detail.productType || 'Type II 20 oz. Evo PVC-free',
         manufacturing: detail.manufacturing || 'USA',
         specs: detail.specs
@@ -353,17 +285,13 @@ async function main() {
 
       for (const cw of detail.colorways) {
         if (cw.sku === mainSku) continue;
-        // Parse color name from the colorway text
-        let cwColor = cw.colorName;
-        if (cwColor) {
-          cwColor = cwColor.replace(new RegExp(`^${patternName}\\s*`, 'i'), '').trim();
-        }
+        const cwColor = cw.colorName;
         allProducts.push({
           mfr_sku: cw.sku,
           pattern_name: patternName,
           color_name: cwColor || null,
           image_url: cw.imgSrc || null,
-          product_url: `${BASE_URL}${cw.href}`,
+          product_url: cw.href,
           product_type: detail.productType || 'Type II 20 oz. Evo PVC-free',
           manufacturing: detail.manufacturing || 'USA',
           specs: detail.specs // Share parent design specs
@@ -427,7 +355,12 @@ async function main() {
   await pool.end();
 }
 
-main().catch(err => {
+module.exports = { scrapeDetailPage, upsertProduct };
+
+if (require.main === module && (process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
+  console.error('Catalog writes require --apply and explicit Steve approval. Use the verification runner for read-only proof.');
+  process.exitCode = 1;
+} else if (require.main === module) main().catch(err => {
   console.error('Fatal error:', err);
   sendSlack(`❌ *Versa Evo PVC-free Scrape FAILED*\n• Error: ${err.message}`);
   pool.end();
diff --git a/DW-Programming/sweep-to-100.js b/DW-Programming/sweep-to-100.js
index a2c0d680..5b065e99 100644
--- a/DW-Programming/sweep-to-100.js
+++ b/DW-Programming/sweep-to-100.js
@@ -16,7 +16,7 @@ const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
 const API_VERSION = '2024-01';
 const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const args = process.argv.slice(2);
-const DRY = args.includes('--dry-run');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
 const CONC = parseInt(args.find((_, i, a) => a[i-1] === '--conc') || '6', 10) || 6;
 
 function palette(url){
diff --git a/DW-Programming/tag-designer-colors.js b/DW-Programming/tag-designer-colors.js
index 3317b6a1..afbc1219 100644
--- a/DW-Programming/tag-designer-colors.js
+++ b/DW-Programming/tag-designer-colors.js
@@ -17,7 +17,7 @@ const args = process.argv.slice(2);
 const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
 const API_VERSION = '2024-01';
 const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const DRY = args.includes('--dry-run');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
 const TOP2 = args.includes('--top2');
 const PREFIX = args.find((_, i, a) => a[i-1] === '--prefix') || 'color:';
 const LIMIT = parseInt(args.find((_, i, a) => a[i-1] === '--limit') || '0', 10) || 0;
diff --git a/DW-Programming/versa-evo-enrich.js b/DW-Programming/versa-evo-enrich.js
index 30c3cbcc..bc3712ed 100644
--- a/DW-Programming/versa-evo-enrich.js
+++ b/DW-Programming/versa-evo-enrich.js
@@ -1,5 +1,6 @@
 const { chromium } = require('playwright');
 const { Client } = require('pg');
+const { extractVersaColorways } = require('./lib/versa-colorway-extraction');
 
 const DB_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified');
 const BASE_URL = 'https://www.versadesignedsurfaces.com';
@@ -15,7 +16,7 @@ async function enrichProducts() {
     WHERE collection = 'Versa 20 oz Evo PVC-free'
       AND product_url IS NOT NULL AND product_url <> ''
       AND (
-        color_name IS NULL OR color_name = ''
+        color_name IS NULL OR color_name = '' OR color_name ~ '^#[0-9A-Fa-f]{3,8}$'
         OR image_url IS NULL OR image_url = '' OR image_url LIKE '%base64%'
       )
     ORDER BY pattern_name, mfr_sku
@@ -42,105 +43,16 @@ async function enrichProducts() {
       await page.evaluate(() => window.scrollTo(0, 300));
       await page.waitForTimeout(1000);
 
-      const data = await page.evaluate((patternName, sku) => {
-        const result = { colorName: '', imageUrl: '' };
-
-        // 1. Get color name from h1 or page title
-        const h1 = document.querySelector('h1');
-        let title = h1 ? h1.textContent.trim() : '';
-        if (!title) {
-          const tEl = document.querySelector('title');
-          title = tEl ? tEl.textContent.trim().split('|')[0].trim() : '';
-        }
-
-        // Extract color by removing pattern name from title
-        if (title && patternName) {
-          let color = title;
-          // Remove pattern name (case-insensitive)
-          const re = new RegExp(patternName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
-          color = color.replace(re, '').trim();
-          // Clean up extra text
-          color = color
-            .replace(/PVC-free/gi, '').replace(/Evo®?/gi, '')
-            .replace(/Type\s*II/gi, '').replace(/20\s*oz\.?/gi, '')
-            .replace(/Versa/gi, '').replace(/Wallcovering/gi, '')
-            .replace(/^\s*[-|]\s*/, '').replace(/\s*[-|]\s*$/, '')
-            .trim();
-          result.colorName = color;
-        }
-
-        // 2. Get real image URL (not base64)
-        // Look for the main product image — typically a large image on the detail page
-        const imgSelectors = [
-          'img[src*="fileadmin"][src*="' + sku.split('-')[0].toLowerCase() + '"]',
-          'img[src*="fileadmin"][src*="AVF"]',
-          'img[src*="fileadmin"][src*="avf"]',
-          'img[src*="csm_"]',
-          '.product-detail img:not([src^="data:"])',
-          'main img:not([src^="data:"])',
-          'img[src*="fileadmin"]:not([src^="data:"])',
-        ];
-
-        for (const sel of imgSelectors) {
-          const imgs = document.querySelectorAll(sel);
-          for (const img of imgs) {
-            const src = img.getAttribute('src') || '';
-            if (src && !src.startsWith('data:') && !src.includes('logo') && !src.includes('icon')) {
-              result.imageUrl = src;
-              return result; // found a good image
-            }
-          }
-        }
-
-        // Fallback: any non-base64 img in main content
-        const allImgs = document.querySelectorAll('img');
-        for (const img of allImgs) {
-          const src = img.getAttribute('src') || '';
-          if (src && !src.startsWith('data:') && src.includes('fileadmin') && !src.includes('logo')) {
-            result.imageUrl = src;
-            break;
-          }
-        }
-
-        return result;
-      }, row.pattern_name, row.mfr_sku);
-
-      // Build update
-      let colorName = data.colorName || row.color_name || '';
-      let imageUrl = data.imageUrl || '';
-
-      // Make image URL absolute
-      if (imageUrl && !imageUrl.startsWith('http')) {
-        imageUrl = `${BASE_URL}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`;
-      }
-
-      // Don't overwrite good data with empty
-      const updates = [];
-      const params = [];
-      let paramIdx = 1;
-
-      if (colorName && (!row.color_name || row.color_name === '')) {
-        updates.push(`color_name = $${paramIdx++}`);
-        params.push(colorName);
-      }
+      const data = await page.evaluate(extractVersaColorways, { pageUrl: page.url() });
+      if (data.main.sku !== row.mfr_sku.toUpperCase()) throw new Error('Unexpected Versa redirect/SKU');
+      const colorName = data.main.colorName;
+      const imageUrl = data.main.imgSrc;
 
-      if (imageUrl && (!row.image_url || row.image_url === '' || row.image_url.includes('base64'))) {
-        updates.push(`image_url = $${paramIdx++}`);
-        params.push(imageUrl);
-      }
-
-      if (updates.length > 0) {
-        updates.push(`updated_at = NOW()`);
-        updates.push(`last_scraped = NOW()`);
-        params.push(row.id);
-        await db.query(
-          `UPDATE versa_catalog SET ${updates.join(', ')} WHERE id = $${paramIdx}`,
-          params
-        );
+      if (await updateProduct(db, row, { colorName, imageUrl })) {
         enriched++;
         console.log(`  -> Color: "${colorName}" | Image: ${imageUrl ? 'YES' : 'NO'}`);
       } else {
-        console.log(`  -> No new data found`);
+        console.log('  -> No new data found');
       }
 
     } catch (err) {
@@ -174,7 +86,42 @@ async function enrichProducts() {
   await db.end();
 }
 
-enrichProducts().catch(err => {
+async function updateProduct(db, row, { colorName, imageUrl }) {
+  const updates = [];
+  const params = [];
+  let paramIdx = 1;
+
+  if (colorName && (!row.color_name || /^#[0-9a-f]{3,8}$/i.test(row.color_name))) {
+    updates.push(`color_name = $${paramIdx++}`);
+    params.push(colorName);
+  }
+
+  if (imageUrl && (!row.image_url || row.image_url.includes('base64'))) {
+    updates.push(`image_url = $${paramIdx++}`);
+    params.push(imageUrl);
+    updates.push(`all_images = $${paramIdx++}`);
+    params.push(JSON.stringify(imageUrl ? [imageUrl] : []));
+  }
+
+  if (updates.length > 0) {
+    updates.push(`updated_at = NOW()`);
+    updates.push(`last_scraped = NOW()`);
+    params.push(row.id);
+    await db.query(
+      `UPDATE versa_catalog SET ${updates.join(', ')} WHERE id = $${paramIdx}`,
+      params
+    );
+    return true;
+  }
+  return false;
+}
+
+module.exports = { updateProduct };
+
+if (require.main === module && (process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
+  console.error('Catalog writes require --apply and explicit Steve approval. Use the verification runner for read-only proof.');
+  process.exitCode = 1;
+} else if (require.main === module) enrichProducts().catch(err => {
   console.error('Fatal error:', err);
   process.exit(1);
 });
diff --git a/DW-Programming/versa-evo-pvcfree-scraper.js b/DW-Programming/versa-evo-pvcfree-scraper.js
index 2e3ebb51..b1f1d272 100644
--- a/DW-Programming/versa-evo-pvcfree-scraper.js
+++ b/DW-Programming/versa-evo-pvcfree-scraper.js
@@ -1,5 +1,6 @@
 const { chromium } = require('playwright');
 const { Client } = require('pg');
+const { extractVersaColorways } = require('./lib/versa-colorway-extraction');
 
 const DB_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified');
 const BASE_URL = 'https://www.versadesignedsurfaces.com';
@@ -52,96 +53,10 @@ async function scrapeCollection() {
       await page.goto(detailUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
       await page.waitForTimeout(3000); // wait for JS rendering
 
-      const productData = await page.evaluate(() => {
-        const data = { title: '', colorways: [], images: [], specs: {} };
-
-        // Page title / h1
-        const h1 = document.querySelector('h1');
-        if (h1) data.title = h1.textContent.trim();
-
-        // Title from <title> tag as fallback
-        if (!data.title) {
-          const titleTag = document.querySelector('title');
-          if (titleTag) data.title = titleTag.textContent.trim().split('|')[0].trim();
-        }
-
-        // Find all colorway links on the page (sibling colors in this pattern)
-        const seenSkus = new Set();
-        document.querySelectorAll('a[href*="/product-detail/"]').forEach(link => {
-          const href = link.getAttribute('href') || '';
-          // Match AVF SKU pattern at end of URL
-          const m = href.match(/\/(avf[\d]+-[\d-]+)$/i);
-          if (m) {
-            const sku = m[1].toUpperCase();
-            if (!seenSkus.has(sku)) {
-              seenSkus.add(sku);
-              // Try to get color name from link text
-              let colorName = '';
-              const textContent = link.textContent.trim();
-              // Filter out generic nav text
-              if (textContent && textContent.length < 80 && !textContent.includes('\n')) {
-                colorName = textContent;
-              }
-              const img = link.querySelector('img');
-              let imgSrc = '';
-              if (img) {
-                // TK-11048 FIX: prefer the REAL lazy-loaded URL (srcset/data-src);
-                // `src` is a base64 data-URI PLACEHOLDER that must NEVER be stored
-                // (this was the original bug that filled versa_catalog with base64).
-                const srcset = img.getAttribute('srcset') || img.getAttribute('data-srcset') || '';
-                const firstSrcset = srcset.split(',')[0].trim().split(/\s+/)[0] || '';
-                imgSrc = img.getAttribute('data-src') || firstSrcset || img.getAttribute('src') || '';
-                if (/^data:|;base64,/i.test(imgSrc)) imgSrc = '';   // hard-reject data: URIs
-              }
-
-              data.colorways.push({ sku, colorName, url: href, imgSrc });
-            }
-          }
-        });
-
-        // Collect all product images
-        document.querySelectorAll('img').forEach(img => {
-          const src = img.getAttribute('src') || img.getAttribute('data-src') || '';
-          if (src && !src.startsWith('data:') &&
-              (src.toLowerCase().includes('avf') || src.toLowerCase().includes('evo') ||
-               src.toLowerCase().includes('pvc-free') || src.includes('csm_'))) {
-            data.images.push(src);
-          }
-        });
-
-        // Try to extract specs from page text
-        const body = document.body?.innerText || '';
-        data.bodySnippet = body.substring(0, 4000);
-
-        return data;
-      });
-
-      // Determine color name from title
-      // Title is like "Kaolin Earthen" — pattern is "Kaolin", color is "Earthen"
-      let colorName = '';
-      const title = productData.title || '';
-      if (title && prod.pattern) {
-        // Remove pattern from title to get color
-        const colorPart = title.replace(new RegExp(`^${prod.pattern}\\s*`, 'i'), '').trim();
-        // Also remove trailing brand/type text
-        colorName = colorPart
-          .replace(/PVC-free/gi, '')
-          .replace(/Evo®?/gi, '')
-          .replace(/Type\s*II/gi, '')
-          .replace(/20\s*oz\.?/gi, '')
-          .replace(/Versa/gi, '')
-          .replace(/Wallcovering/gi, '')
-          .trim();
-      }
-
-      // Get main image URL
-      let imageUrl = '';
-      if (productData.images.length > 0) {
-        imageUrl = productData.images[0];
-        if (imageUrl && !imageUrl.startsWith('http')) {
-          imageUrl = `${BASE_URL}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`;
-        }
-      }
+      const productData = await page.evaluate(extractVersaColorways, { pageUrl: page.url() });
+      if (productData.main.sku !== prod.sku) throw new Error('Unexpected Versa redirect/SKU');
+      const colorName = productData.main.colorName;
+      const imageUrl = productData.main.imgSrc;
 
       const product = {
         mfr_sku: prod.sku,
@@ -173,18 +88,9 @@ async function scrapeCollection() {
         // Only include if it's an AVF SKU that starts with same prefix
         if (!cw.sku.startsWith('AVF')) continue;
 
-        let cwImg = cw.imgSrc || '';
-        if (cwImg && !cwImg.startsWith('http')) {
-          cwImg = `${BASE_URL}${cwImg.startsWith('/') ? '' : '/'}${cwImg}`;
-        }
-
-        // Clean up colorway name
-        let cwColor = cw.colorName || '';
-        if (cwColor) {
-          cwColor = cwColor.replace(prod.pattern, '').replace(/PVC-free/gi, '').replace(/Evo®?/gi, '').trim();
-        }
-
-        const cwUrl = cw.url ? (cw.url.startsWith('http') ? cw.url : `${BASE_URL}${cw.url}`) : '';
+        const cwImg = cw.imgSrc;
+        const cwColor = cw.colorName;
+        const cwUrl = cw.url;
 
         allProducts.push({
           ...product,
@@ -198,27 +104,8 @@ async function scrapeCollection() {
 
     } catch (err) {
       console.error(`  ERROR: ${err.message}`);
-      // Still insert with what we know from the listing
-      allProducts.push({
-        mfr_sku: prod.sku,
-        pattern_name: prod.pattern,
-        color_name: '',
-        collection: COLLECTION_NAME,
-        brand: 'Versa Designed Surfaces',
-        product_type: 'Type II 20 oz PVC-free',
-        width: '54"',
-        weight: '20 oz',
-        material: 'Evo PVC-free',
-        fire_rating: '',
-        finish: '',
-        cleaning_code: '',
-        backing: '',
-        match_type: '',
-        image_url: '',
-        product_url: `${BASE_URL}/north-america/products/product-detail/${prod.slug}`,
-        manufacturing_location: 'USA',
-        sustainability: 'PVC-free (Evo technology)',
-      });
+      // Failed pages are skipped; do not replace verified rows with guesses.
+
     }
 
     // Rate limit
@@ -233,13 +120,40 @@ async function scrapeCollection() {
 
   for (const p of allProducts) {
     try {
-      const result = await db.query(`
+      const result = await upsertProduct(db, p);
+
+      if (result.rows[0]?.is_insert) {
+        inserted++;
+        console.log(`  INSERT: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
+      } else {
+        updated++;
+        console.log(`  UPDATE: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
+      }
+    } catch (err) {
+      errors++;
+      console.error(`  DB ERROR [${p.mfr_sku}]: ${err.message}`);
+    }
+  }
+
+  console.log(`\n========== RESULTS ==========`);
+  console.log(`Total processed: ${allProducts.length}`);
+  console.log(`Inserted (new): ${inserted}`);
+  console.log(`Updated (existing): ${updated}`);
+  console.log(`Errors: ${errors}`);
+  console.log(`Collection: ${COLLECTION_NAME}`);
+  console.log(`==============================`);
+
+  await db.end();
+}
+
+async function upsertProduct(db, p) {
+  return db.query(`
         INSERT INTO versa_catalog (
           mfr_sku, pattern_name, color_name, collection, brand, product_type,
           width, weight, material, fire_rating, finish, cleaning_code, backing,
           match_type, image_url, product_url, manufacturing_location, sustainability,
-          last_scraped, updated_at
-        ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,NOW(),NOW())
+          all_images, last_scraped, updated_at
+        ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,NOW(),NOW())
         ON CONFLICT (mfr_sku) DO UPDATE SET
           pattern_name = COALESCE(NULLIF(EXCLUDED.pattern_name,''), versa_catalog.pattern_name),
           color_name = COALESCE(NULLIF(EXCLUDED.color_name,''), versa_catalog.color_name),
@@ -250,6 +164,7 @@ async function scrapeCollection() {
           weight = COALESCE(NULLIF(EXCLUDED.weight,''), versa_catalog.weight),
           material = COALESCE(NULLIF(EXCLUDED.material,''), versa_catalog.material),
           image_url = COALESCE(NULLIF(EXCLUDED.image_url,''), versa_catalog.image_url),
+          all_images = COALESCE(NULLIF(EXCLUDED.all_images,'[]'), versa_catalog.all_images),
           product_url = COALESCE(NULLIF(EXCLUDED.product_url,''), versa_catalog.product_url),
           manufacturing_location = COALESCE(NULLIF(EXCLUDED.manufacturing_location,''), versa_catalog.manufacturing_location),
           sustainability = COALESCE(NULLIF(EXCLUDED.sustainability,''), versa_catalog.sustainability),
@@ -259,34 +174,17 @@ async function scrapeCollection() {
       `, [
         p.mfr_sku, p.pattern_name, p.color_name, p.collection, p.brand, p.product_type,
         p.width, p.weight, p.material, p.fire_rating, p.finish, p.cleaning_code, p.backing,
-        p.match_type, p.image_url, p.product_url, p.manufacturing_location, p.sustainability
+        p.match_type, p.image_url, p.product_url, p.manufacturing_location, p.sustainability,
+        JSON.stringify(p.image_url ? [p.image_url] : [])
       ]);
-
-      if (result.rows[0]?.is_insert) {
-        inserted++;
-        console.log(`  INSERT: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
-      } else {
-        updated++;
-        console.log(`  UPDATE: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
-      }
-    } catch (err) {
-      errors++;
-      console.error(`  DB ERROR [${p.mfr_sku}]: ${err.message}`);
-    }
-  }
-
-  console.log(`\n========== RESULTS ==========`);
-  console.log(`Total processed: ${allProducts.length}`);
-  console.log(`Inserted (new): ${inserted}`);
-  console.log(`Updated (existing): ${updated}`);
-  console.log(`Errors: ${errors}`);
-  console.log(`Collection: ${COLLECTION_NAME}`);
-  console.log(`==============================`);
-
-  await db.end();
 }
 
-scrapeCollection().catch(err => {
+module.exports = { upsertProduct };
+
+if (require.main === module && (process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
+  console.error('Catalog writes require --apply and explicit Steve approval. Use the verification runner for read-only proof.');
+  process.exitCode = 1;
+} else if (require.main === module) scrapeCollection().catch(err => {
   console.error('Fatal error:', err);
   process.exit(1);
 });
diff --git a/scripts/dwtt-body-to-metafields.js b/scripts/dwtt-body-to-metafields.js
index 77925d8b..8046b751 100644
--- a/scripts/dwtt-body-to-metafields.js
+++ b/scripts/dwtt-body-to-metafields.js
@@ -10,7 +10,7 @@ const STORE = 'designer-laboratory-sandbox.myshopify.com';
 const API = `https://${STORE}/admin/api/2024-01`;
 
 const args = process.argv.slice(2);
-const DRY_RUN = args.includes('--dry-run');
+const DRY_RUN = (args.includes('--dry-run') || !args.includes('--apply'));
 const BATCH = parseInt(args[args.indexOf('--batch') + 1]) || 250;
 const AFTER_CURSOR = args.includes('--after') ? args[args.indexOf('--after') + 1] : null;
 
diff --git a/scripts/interior-design-tags.js b/scripts/interior-design-tags.js
index f6699f0e..425db09a 100644
--- a/scripts/interior-design-tags.js
+++ b/scripts/interior-design-tags.js
@@ -18,6 +18,7 @@
  *   node scripts/interior-design-tags.js --limit 50         # Limit per table
  *   node scripts/interior-design-tags.js --shopify-only     # Skip Gemini, just push to Shopify
  *   node scripts/interior-design-tags.js --dry-run          # Preview without saving
+ *   Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  */
 
 const { Pool } = require('pg');
@@ -503,7 +504,7 @@ async function processTable(tableName, limit, dryRun, shopifyOnly) {
 
 async function main() {
   const args = process.argv.slice(2);
-  const dryRun = args.includes('--dry-run');
+  const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
   const shopifyOnly = args.includes('--shopify-only');
   const tableFilter = args.find((a, i) => args[i - 1] === '--table');
   const limitArg = args.find((a, i) => args[i - 1] === '--limit');
diff --git a/scripts/populate-mfr-sku-metafield.js b/scripts/populate-mfr-sku-metafield.js
index 5cb5b3bb..1a85120b 100644
--- a/scripts/populate-mfr-sku-metafield.js
+++ b/scripts/populate-mfr-sku-metafield.js
@@ -11,6 +11,7 @@
  * 3. Handle patterns
  *
  * Usage: node populate-mfr-sku-metafield.js [--dry-run] [--vendor=VendorName]
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  */
 
 const SHOPIFY_STORE = 'designer-laboratory-sandbox';
@@ -184,7 +185,7 @@ function determineMfrSku(product) {
 
 async function main() {
   const args = process.argv.slice(2);
-  const dryRun = args.includes('--dry-run');
+  const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
   const vendorArg = args.find(a => a.startsWith('--vendor='));
   const vendorFilter = vendorArg ? vendorArg.split('=')[1] : null;
 
diff --git a/scripts/rw-upload-room-settings.js b/scripts/rw-upload-room-settings.js
index f515e820..a4d606bc 100644
--- a/scripts/rw-upload-room-settings.js
+++ b/scripts/rw-upload-room-settings.js
@@ -32,7 +32,7 @@ const pool = new Pool({
 const args = process.argv.slice(2);
 const batchSize = parseInt(args.find((a,i) => args[i-1] === '--batch') || '50');
 const startId = parseInt(args.find((a,i) => args[i-1] === '--start-id') || '0');
-const dryRun = args.includes('--dry-run');
+const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
 
 /**
  * Upload file to Shopify via staged upload + file create
diff --git a/scripts/standardize-product-types-bulk.js b/scripts/standardize-product-types-bulk.js
index ff2815e7..61114b19 100644
--- a/scripts/standardize-product-types-bulk.js
+++ b/scripts/standardize-product-types-bulk.js
@@ -14,7 +14,7 @@ const API_VERSION = '2024-10';
 const GQL_URL = `https://${SHOPIFY_DOMAIN}/admin/api/${API_VERSION}/graphql.json`;
 const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL || '';
 
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const fs = require('fs');
 const path = require('path');
 
diff --git a/scripts/standardize-product-types.js b/scripts/standardize-product-types.js
index ee838791..27d84e1c 100644
--- a/scripts/standardize-product-types.js
+++ b/scripts/standardize-product-types.js
@@ -14,7 +14,7 @@ const API_VERSION = '2024-10';
 const GQL_URL = `https://${SHOPIFY_DOMAIN}/admin/api/${API_VERSION}/graphql.json`;
 const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL || '';
 
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const BATCH_FILTER = process.argv.find(a => a.startsWith('--batch='))?.split('=')[1] || null;
 
 // Rate limiting — Shopify Plus gets 1000pts/s, standard gets 100pts/s
diff --git a/scripts/wallquest-refresh/publish-grasscloth-pr.cjs b/scripts/wallquest-refresh/publish-grasscloth-pr.cjs
index 9309fc13..421d3bb1 100644
--- a/scripts/wallquest-refresh/publish-grasscloth-pr.cjs
+++ b/scripts/wallquest-refresh/publish-grasscloth-pr.cjs
@@ -14,7 +14,7 @@ const PSQL = '/opt/homebrew/opt/postgresql@14/bin/psql';
 const q = sql => execSync(`PGPASSWORD='${PGPW}' ${PSQL} -h localhost -U dw_admin -d dw_unified -tAc "${sql.replace(/"/g, '\\"')}"`, { encoding: 'utf8', maxBuffer: 1e8 });
 
 const args = process.argv.slice(2);
-const DRY = args.includes('--dry');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
 const PILOT = args.includes('--pilot');
 const ONE = (args[args.indexOf('--sku') + 1] && args.includes('--sku')) ? args[args.indexOf('--sku') + 1] : null;
 const LIMIT = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1]) : null;
diff --git a/shopify/scripts/add-missing-samples-v2.js b/shopify/scripts/add-missing-samples-v2.js
index 60d28ec1..119851f0 100644
--- a/shopify/scripts/add-missing-samples-v2.js
+++ b/shopify/scripts/add-missing-samples-v2.js
@@ -10,6 +10,7 @@
  *   node add-missing-samples-v2.js              # Run (resumes from progress)
  *   node add-missing-samples-v2.js --fresh      # Ignore progress, full rescan
  *   node add-missing-samples-v2.js --dry-run    # Scan only, no creates
+ *   Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  *   node add-missing-samples-v2.js --scan-only  # Scan + report, skip creates
  */
 
@@ -358,7 +359,7 @@ async function createMissingVariants(progress) {
 // ─── Main ──────────────────────────────────────────────────────────────
 async function main() {
   const args = process.argv.slice(2);
-  const dryRun = args.includes('--dry-run');
+  const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
   const scanOnly = args.includes('--scan-only');
   const fresh = args.includes('--fresh');
 
diff --git a/shopify/scripts/cadence/kravet-3b-pricefix.mjs b/shopify/scripts/cadence/kravet-3b-pricefix.mjs
index 7dca9390..90717142 100644
--- a/shopify/scripts/cadence/kravet-3b-pricefix.mjs
+++ b/shopify/scripts/cadence/kravet-3b-pricefix.mjs
@@ -59,7 +59,7 @@ for (const t of targets) {
 }
 console.log(`Undo CSV written: ${UNDO}`);
 
-const DRY = process.argv.includes('--dry');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 if (DRY) { console.log('DRY RUN — no writes'); process.exit(0); }
 
 let ok = 0; const failures = [];
diff --git a/shopify/scripts/create-missing-sample-variants.js b/shopify/scripts/create-missing-sample-variants.js
index 6918f906..aed8d587 100644
--- a/shopify/scripts/create-missing-sample-variants.js
+++ b/shopify/scripts/create-missing-sample-variants.js
@@ -9,6 +9,7 @@
  *   node create-missing-sample-variants.js           # Start/resume from progress file
  *   node create-missing-sample-variants.js --fresh    # Ignore progress, start from beginning
  *   node create-missing-sample-variants.js --dry-run  # Just show what would be created
+ *   Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  */
 
 const https = require('https');
@@ -124,7 +125,7 @@ async function setInventory(inventoryItemId, qty = 2026) {
 
 async function main() {
   const args = process.argv.slice(2);
-  const dryRun = args.includes('--dry-run');
+  const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
   const fresh = args.includes('--fresh');
 
   console.log(`\n${'═'.repeat(60)}`);
diff --git a/shopify/scripts/dwvs-reconcile-mfr.js b/shopify/scripts/dwvs-reconcile-mfr.js
index 3f2c010e..bd4bc4a9 100644
--- a/shopify/scripts/dwvs-reconcile-mfr.js
+++ b/shopify/scripts/dwvs-reconcile-mfr.js
@@ -15,7 +15,7 @@ const fs = require('fs');
 const path = require('path');
 const { execSync } = require('child_process');
 
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 
 async function gql(q, v = {}) {
   const r = await fetch(ENDPOINT, {
diff --git a/shopify/scripts/innovations-pattern-sweep.js b/shopify/scripts/innovations-pattern-sweep.js
index 9a40be67..1cbb3e15 100644
--- a/shopify/scripts/innovations-pattern-sweep.js
+++ b/shopify/scripts/innovations-pattern-sweep.js
@@ -15,7 +15,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
 if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
 const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 
 async function gql(q, v = {}) {
   for (let a = 0; a < 6; a++) {
diff --git a/shopify/scripts/innovations-sitemap-sweep.js b/shopify/scripts/innovations-sitemap-sweep.js
index 58b63a55..26a3274b 100644
--- a/shopify/scripts/innovations-sitemap-sweep.js
+++ b/shopify/scripts/innovations-sitemap-sweep.js
@@ -14,7 +14,7 @@ const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
 const SITEMAP = 'https://www.innovationsusa.com/sitemap.xml';
 if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
 const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 
 async function gql(q, v = {}) {
   for (let a = 0; a < 6; a++) {
diff --git a/shopify/scripts/reconcile/fix-live-shopify-product-type.js b/shopify/scripts/reconcile/fix-live-shopify-product-type.js
index 46ab6b78..0cb9c932 100644
--- a/shopify/scripts/reconcile/fix-live-shopify-product-type.js
+++ b/shopify/scripts/reconcile/fix-live-shopify-product-type.js
@@ -27,7 +27,7 @@
 const fs = require('fs');
 const path = require('path');
 
-const DRY = process.argv.includes('--dry');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
 const API = '2024-10';
 
diff --git a/shopify/scripts/sandberg-add-bolt-variants.js b/shopify/scripts/sandberg-add-bolt-variants.js
index ed1ceca1..7aaad952 100644
--- a/shopify/scripts/sandberg-add-bolt-variants.js
+++ b/shopify/scripts/sandberg-add-bolt-variants.js
@@ -99,7 +99,7 @@ async function processOne(row, dry) {
 }
 
 async function run() {
-  const dry = process.argv.includes('--dry-run');
+  const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
   console.log(`${dry ? '[DRY RUN] ' : ''}Adding bolt variants to ${ROLLS.length} Sandberg products.`);
   console.log(`  option: "${OPTION_NAME}" = "${BOLT_OPTION_VALUE}"`);
   console.log(`  weight: ${TARGET_WEIGHT_LB} lb`);
diff --git a/shopify/scripts/sandberg-add-mural-variants.js b/shopify/scripts/sandberg-add-mural-variants.js
index 9fffef3b..2f02d8ec 100644
--- a/shopify/scripts/sandberg-add-mural-variants.js
+++ b/shopify/scripts/sandberg-add-mural-variants.js
@@ -70,7 +70,7 @@ async function processOne(row, dry) {
 }
 
 async function run() {
-  const dry = process.argv.includes('--dry-run');
+  const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
   console.log(`${dry ? '[DRY RUN] ' : ''}Adding m² variants to ${ITEMS.length} Sandberg murals.`);
 
   const results = [];
diff --git a/shopify/scripts/sandberg-william-morris-weight-4lb.js b/shopify/scripts/sandberg-william-morris-weight-4lb.js
index b2e09201..8d15329c 100644
--- a/shopify/scripts/sandberg-william-morris-weight-4lb.js
+++ b/shopify/scripts/sandberg-william-morris-weight-4lb.js
@@ -116,7 +116,7 @@ async function run() {
 
   fs.writeFileSync(path.join(outDir, `sandberg-wm-plan-${stamp}.json`), JSON.stringify({ bolts, sampleOnly }, null, 2));
 
-  if (process.argv.includes('--dry-run')) {
+  if ((process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
     console.log('Dry run — no updates made. Plan written.');
     return;
   }
diff --git a/shopify/scripts/sandberg-wm-add-wallcovering-title.js b/shopify/scripts/sandberg-wm-add-wallcovering-title.js
index 3591fba2..9efd531f 100644
--- a/shopify/scripts/sandberg-wm-add-wallcovering-title.js
+++ b/shopify/scripts/sandberg-wm-add-wallcovering-title.js
@@ -89,7 +89,7 @@ async function run() {
 
   fs.writeFileSync(path.join(outDir, `sandberg-wm-title-plan-${stamp}.json`), JSON.stringify({ plan, skipped }, null, 2));
 
-  if (process.argv.includes('--dry-run')) {
+  if ((process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
     console.log('[title] Dry run — no updates made.');
     if (plan.length) console.log('[title] Sample:', plan.slice(0, 5));
     return;
diff --git a/shopify/scripts/thibaut-disco-draft.js b/shopify/scripts/thibaut-disco-draft.js
index 5729a6e9..2b8c8a53 100644
--- a/shopify/scripts/thibaut-disco-draft.js
+++ b/shopify/scripts/thibaut-disco-draft.js
@@ -16,7 +16,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
 if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
 const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 const LIM = parseInt((process.argv.find(a=>a.startsWith('--limit='))||'--limit=0').split('=')[1]) || 0;
 
 async function gql(q, v = {}) {
diff --git a/shopify/scripts/thibaut-photo-fix.js b/shopify/scripts/thibaut-photo-fix.js
index 5fc71dff..78f43049 100644
--- a/shopify/scripts/thibaut-photo-fix.js
+++ b/shopify/scripts/thibaut-photo-fix.js
@@ -10,6 +10,7 @@
  *            present does it delete the recorded low-res media ids (so the
  *            2520 becomes the sole / featured image). Safe + idempotent.
  *   --dry-run  with either pass: report, no writes.
+ *   Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
  */
 const fs = require('fs');
 const path = require('path');
@@ -129,7 +130,7 @@ async function doPrune(dry) {
 }
 
 (async () => {
-  const dry = process.argv.includes('--dry-run');
+  const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
   if (process.argv.includes('--prune')) await doPrune(dry);
   else await doAdd(dry); // default = add
 })().catch(e => { console.error('\nFATAL', e); process.exit(1); });
diff --git a/shopify/scripts/thibaut-spec-backfill.js b/shopify/scripts/thibaut-spec-backfill.js
index dc69e0f8..d1ea730d 100644
--- a/shopify/scripts/thibaut-spec-backfill.js
+++ b/shopify/scripts/thibaut-spec-backfill.js
@@ -12,7 +12,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
 const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
 if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
 const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 
 async function gql(q, v = {}) {
   for (let a = 0; a < 6; a++) {
diff --git a/shopify/scripts/vendor-disco-draft.js b/shopify/scripts/vendor-disco-draft.js
index d567a6fa..9bac3045 100644
--- a/shopify/scripts/vendor-disco-draft.js
+++ b/shopify/scripts/vendor-disco-draft.js
@@ -15,7 +15,7 @@ const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
 if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
 const sleep = ms => new Promise(r => setTimeout(r, ms));
 const arg = k => (process.argv.find(a => a.startsWith(`--${k}=`)) || '').split('=').slice(1).join('=');
-const VENDOR = arg('vendor'), TABLE = arg('table'), DRY = process.argv.includes('--dry-run');
+const VENDOR = arg('vendor'), TABLE = arg('table'), DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
 if (!VENDOR || !TABLE) { console.error('need --vendor and --table'); process.exit(1); }
 
 async function gql(q, v = {}) {
diff --git a/shopify/scripts/wm-add-bolt-variants.js b/shopify/scripts/wm-add-bolt-variants.js
index 7d9c954d..f1221106 100644
--- a/shopify/scripts/wm-add-bolt-variants.js
+++ b/shopify/scripts/wm-add-bolt-variants.js
@@ -60,7 +60,7 @@ async function processOne(row, dry) {
 }
 
 async function run() {
-  const dry = process.argv.includes('--dry-run');
+  const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
   console.log(`${dry ? '[DRY] ' : ''}Adding Bolt variants to ${TARGETS.length} William Morris products at $${PRICE_USD}`);
   const results = [];
   for (const row of TARGETS) {

← 2fafce86 fix(review): 6 code-review findings — Versa color, price-gat  ·  back to Designer Wallcoverings  ·  auto-data-snapshot: 2026-09-14T15:41:36 (3 data files) — sho 57ba59ec →