[object Object]

← back to Designer Wallcoverings

Newwall width re-crawl: public-HTML spec-table scraper (DTD-A), validated on 25/6-vendor sample

63dcdeeea8690f580749b2a4c06036a2030ef191 · 2026-06-20 12:34:21 -0700 · Steve

Files touched

Diff

commit 63dcdeeea8690f580749b2a4c06036a2030ef191
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 20 12:34:21 2026 -0700

    Newwall width re-crawl: public-HTML spec-table scraper (DTD-A), validated on 25/6-vendor sample
---
 .../found-2026-06-20T19-33-13-642Z.validate.jsonl  |   9 +
 .../found-2026-06-20T19-33-58-365Z.validate.jsonl  |  10 ++
 ...skipped-2026-06-20T19-33-13-642Z.validate.jsonl |  16 ++
 ...skipped-2026-06-20T19-33-58-365Z.validate.jsonl |  15 ++
 shopify/scripts/cadence/recrawl-newwall-width.js   | 199 +++++++++++++++++++++
 5 files changed, 249 insertions(+)

diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-13-642Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-13-642Z.validate.jsonl
new file mode 100644
index 00000000..fbe6ddb8
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-13-642Z.validate.jsonl
@@ -0,0 +1,9 @@
+{"dw_sku":"DWXW-1005230","handle":"a06-cb-01w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005233","handle":"a06-cb-04w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005231","handle":"cloudbusting-a06-cb-02w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005232","handle":"cloudbusting-a06-cb-03w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1013126","handle":"beltane-hemp-cosmos","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013125","handle":"beltane-hemp-euphorbia","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1008144","handle":"camelot-stripe-3","vendor":"House of Hackney","dim":"53.5” wide","cov":"","fmt":"Yard","width_in":53.5,"canonical":"53.5 Inches"}
+{"dw_sku":"DWXW-1013153","handle":"heavenly-hemp-amber","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013152","handle":"heavenly-hemp-anemone","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-58-365Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-58-365Z.validate.jsonl
new file mode 100644
index 00000000..1ec59f00
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-58-365Z.validate.jsonl
@@ -0,0 +1,10 @@
+{"dw_sku":"DWXW-1005230","handle":"a06-cb-01w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005233","handle":"a06-cb-04w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005231","handle":"cloudbusting-a06-cb-02w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005232","handle":"cloudbusting-a06-cb-03w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1013126","handle":"beltane-hemp-cosmos","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013125","handle":"beltane-hemp-euphorbia","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1008144","handle":"camelot-stripe-3","vendor":"House of Hackney","dim":"53.5” wide","cov":"","fmt":"Yard","width_in":53.5,"canonical":"53.5 Inches"}
+{"dw_sku":"DWXW-1013153","handle":"heavenly-hemp-amber","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013152","handle":"heavenly-hemp-anemone","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1012918","handle":"casa-di-tartan-15568","vendor":"Masi x Newwall","dim":"20.5\" wide x 11 yards","cov":"50 sq. ft.","fmt":"Roll","width_in":20.5,"canonical":"20.5 Inches"}
diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-13-642Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-13-642Z.validate.jsonl
new file mode 100644
index 00000000..2114ea9b
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-13-642Z.validate.jsonl
@@ -0,0 +1,16 @@
+{"dw_sku":"DWXW-1005696","handle":"6500013-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005697","handle":"6500014-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005698","handle":"6500015-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005699","handle":"6500016-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005700","handle":"6500017-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012918","handle":"casa-di-tartan-15568","vendor":"Masi x Newwall","reason":"coverage_mismatch(dim=20.5\" wide x 11 yards,cov=50 sq. ft.)"}
+{"dw_sku":"DWXW-1011674","handle":"abstract-mex-blue","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011673","handle":"abstract-mex-coral","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011675","handle":"abstract-mex-orange","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011676","handle":"abstract-mex-purple","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007035","handle":"acassia","vendor":"Tres Tintas","reason":"unparsed:Line drawing required"}
+{"dw_sku":"DWXW-1007683","handle":"70s-wd702001","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007684","handle":"70s-wd702002","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012412","handle":"a-new-day-wdnd2501","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012413","handle":"a-new-day-wdnd2502","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1009818","handle":"a-sign-in-the-water","vendor":"Wall&decò","reason":"mural_custom"}
diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-58-365Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-58-365Z.validate.jsonl
new file mode 100644
index 00000000..28706c1b
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-58-365Z.validate.jsonl
@@ -0,0 +1,15 @@
+{"dw_sku":"DWXW-1005696","handle":"6500013-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005697","handle":"6500014-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005698","handle":"6500015-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005699","handle":"6500016-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005700","handle":"6500017-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011674","handle":"abstract-mex-blue","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011673","handle":"abstract-mex-coral","vendor":"Tres Tintas","reason":"http_503"}
+{"dw_sku":"DWXW-1011675","handle":"abstract-mex-orange","vendor":"Tres Tintas","reason":"http_503"}
+{"dw_sku":"DWXW-1011676","handle":"abstract-mex-purple","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007035","handle":"acassia","vendor":"Tres Tintas","reason":"unparsed:Line drawing required"}
+{"dw_sku":"DWXW-1007683","handle":"70s-wd702001","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007684","handle":"70s-wd702002","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012412","handle":"a-new-day-wdnd2501","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012413","handle":"a-new-day-wdnd2502","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1009818","handle":"a-sign-in-the-water","vendor":"Wall&decò","reason":"mural_custom"}
diff --git a/shopify/scripts/cadence/recrawl-newwall-width.js b/shopify/scripts/cadence/recrawl-newwall-width.js
new file mode 100644
index 00000000..ec727d44
--- /dev/null
+++ b/shopify/scripts/cadence/recrawl-newwall-width.js
@@ -0,0 +1,199 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * recrawl-newwall-width.js — LOCAL reversible Newwall WIDTH re-crawl + backfill
+ * for the genuine crawl-gap cohort (vp-dw-commerce, 2026-06-20; DTD verdict A, 3/3).
+ *
+ * THE GAP: 1,922 net-new costed Newwall rows in Mac2-LOCAL dw_unified.newwall_catalog
+ * (6 confirmed lines: Tres Tintas, Coordonné, Wall&decò, House of Hackney, 17 Patterns,
+ * Masi x Newwall) have NO width anywhere → they fail validate-before-activate.js
+ * (width HARD-required) and stay DRAFT.
+ *
+ * WHERE WIDTH ACTUALLY LIVES (verified read-only 2026-06-20): NOT in Newwall's
+ * /products/{handle}.js feed (it carries no width, auth or not). It IS in the
+ * PUBLIC product-page server HTML spec table under label "Dimensions"
+ * (e.g. "Dimensions: 54" = 54 inches; cross-validated by "Wall coverage: 13.5 sq. ft."
+ * = 54" × 36" = 1 yard). No login, no reCAPTCHA, no headless browser, $0.
+ *
+ * RULES:
+ *   - NEVER invent width. Made-to-Measure Mural variants render "Dimensions: Custom"
+ *     (no fixed roll width) → those rows are LEFT UNTOUCHED (stay DRAFT, correctly).
+ *   - Only accept a plausible inch value (10–250"). The "Dimensions" field on these
+ *     Newwall lines is the roll/panel WIDTH in inches (54", 27", 36", 21" …).
+ *   - Optional sanity check: if "Wall coverage" (sq ft) is present, width ≈
+ *     coverage*144 / 36 (1 yard length) — used only to reject implausible parses,
+ *     never to derive a width.
+ *   - Writes width + width_inches to Mac2-LOCAL dw_unified ONLY (reversible).
+ *     --dry-run by default; every pre-change value captured in the audit snapshot.
+ *   - Politeness: ~1 req/sec, 1 retry on transient failure, realistic UA.
+ *
+ * Usage:
+ *   node recrawl-newwall-width.js --validate         # crawl 30 sample products, NO DB write, print parse
+ *   node recrawl-newwall-width.js                     # DRY-RUN full crawl (no DB write), writes change manifest
+ *   node recrawl-newwall-width.js --commit            # full crawl + apply width UPDATEs (reversible)
+ *   node recrawl-newwall-width.js --limit 100         # cap rows (testing)
+ */
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const args = process.argv.slice(2);
+const COMMIT = args.includes('--commit');
+const VALIDATE = args.includes('--validate');
+const LIMIT = (() => { const i = args.indexOf('--limit'); return i >= 0 ? parseInt(args[i + 1], 10) : null; })();
+
+const VENDORS = "('Coordonné','17 Patterns','House of Hackney','Masi x Newwall','Tres Tintas','Wall&decò')";
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
+const MIN_IN = 10, MAX_IN = 250;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function psql(sql) {
+  return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', sql], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim();
+}
+
+function plausible(n) { return Number.isFinite(n) && n >= MIN_IN && n <= MAX_IN; }
+
+// Parse the label:value spec table out of a Newwall product-page HTML string.
+function parseSpecs(html) {
+  const pairs = {};
+  for (const m of html.matchAll(/>([A-Z][A-Za-z \/]{2,28}):\s*<\/[^>]+>\s*(?:<[^>]+>\s*)*([^<]{1,60})</g)) {
+    const k = m[1].trim(), v = m[2].trim();
+    if (!(k in pairs)) pairs[k] = v;   // first occurrence wins
+  }
+  return pairs;
+}
+
+// Pull a plausible inch WIDTH from the "Dimensions" spec value. Returns number or NaN.
+// Accepts: "54", "54\"", "54 in", "27 Inches", "20.5". Rejects "Custom", "", non-numeric.
+function widthFromDimensions(dim) {
+  if (!dim) return NaN;
+  const s = String(dim).trim();
+  if (/custom/i.test(s)) return NaN;          // made-to-measure mural → no fixed width
+  const m = s.match(/(\d+(?:\.\d+)?)/);       // leading number
+  if (!m) return NaN;
+  return parseFloat(m[1]);
+}
+
+// Coverage cross-check: "13.5 sq. ft." with 1-yard (36") length implies width(in) = cov*144/36.
+function coverageWidth(cov) {
+  if (!cov) return NaN;
+  const m = String(cov).match(/(\d+(?:\.\d+)?)/);
+  if (!m) return NaN;
+  const sqft = parseFloat(m[1]);
+  return Number.isFinite(sqft) ? (sqft * 144) / 36 : NaN;
+}
+
+function canonical(inches) {
+  const r = Math.round(inches * 100) / 100;
+  return `${r % 1 === 0 ? r : r} Inches`;
+}
+
+async function fetchHtml(handle) {
+  const url = `https://newwall.com/products/${encodeURIComponent(handle)}`;
+  for (let a = 0; a < 2; a++) {
+    try {
+      const r = await fetch(url, { headers: { 'User-Agent': UA, 'Accept': 'text/html' } });
+      if (r.status === 429) { await sleep(3000 * (a + 1)); continue; }
+      if (!r.ok) return { status: r.status, html: null };
+      return { status: r.status, html: await r.text() };
+    } catch (e) { if (a === 1) return { status: 0, html: null, err: e.message }; await sleep(1500); }
+  }
+  return { status: 0, html: null };
+}
+
+(async () => {
+  // 1. load the gap cohort
+  const where = `coalesce(shopify_product_id::text,'')='' AND price_trade>0
+    AND coalesce(width,'')='' AND coalesce(width_inches::text,'')=''
+    AND vendor_name IN ${VENDORS} AND coalesce(handle,'')<>''`;
+  let rows = psql(`SELECT dw_sku, handle, vendor_name FROM newwall_catalog WHERE ${where} ORDER BY vendor_name, handle;`)
+    .split('\n').filter(Boolean).map(l => { const [dw_sku, handle, vendor] = l.split('\t'); return { dw_sku, handle, vendor }; });
+
+  if (VALIDATE) {
+    // 30 across all 6 vendors: 5 each (or as many as exist)
+    const byV = {}; for (const r of rows) (byV[r.vendor] ||= []).push(r);
+    rows = Object.values(byV).flatMap(list => list.slice(0, 5));
+    console.log(`VALIDATE mode: ${rows.length} sample products across ${Object.keys(byV).length} vendors (NO DB write)\n`);
+  } else if (LIMIT) rows = rows.slice(0, LIMIT);
+
+  console.log(`crawling ${rows.length} Newwall product pages (polite ~1/sec)…\n`);
+
+  const found = [];     // {dw_sku, vendor, handle, dim, cov, width_in, canonical}
+  const noWidth = [];   // {dw_sku, vendor, handle, reason}
+  let httpErr = 0, i = 0;
+
+  for (const r of rows) {
+    i++;
+    const { status, html } = await fetchHtml(r.handle);
+    if (!html) { httpErr++; noWidth.push({ ...r, reason: `http_${status}` }); if (!VALIDATE) await sleep(1000); continue; }
+    const specs = parseSpecs(html);
+    const dim = specs['Dimensions'] || '';
+    const cov = specs['Wall coverage'] || '';
+    const fmt = specs['Format available'] || '';
+    let w = widthFromDimensions(dim);
+    // coverage sanity: ONLY valid for Format="Yard" (length = 1 yard = 36").
+    // Roll-format products are multi-yard (e.g. "x 11 yards"), so the 36" assumption
+    // is wrong for them — never reject a Roll width on coverage (would drop valid widths
+    // like Masi "20.5" wide x 11 yards"). Reject only a gross (>2x) Yard-format mismatch.
+    const cw = coverageWidth(cov);
+    let rejected = false;
+    if (/^yard$/i.test(fmt) && plausible(w) && plausible(cw) && (w / cw > 2 || cw / w > 2)) { rejected = true; }
+    if (plausible(w) && !rejected) {
+      found.push({ ...r, dim, cov, fmt, width_in: w, canonical: canonical(w) });
+    } else {
+      noWidth.push({ ...r, reason: rejected ? `coverage_mismatch(dim=${dim},cov=${cov})` : (/custom/i.test(dim) ? 'mural_custom' : (dim ? `unparsed:${dim}` : 'no_dimensions_field')) });
+    }
+    if (VALIDATE) {
+      const tag = plausible(w) && !rejected ? `WIDTH=${canonical(w)}` : `SKIP(${noWidth[noWidth.length-1].reason})`;
+      console.log(`  [${r.vendor}] ${r.handle.padEnd(34)} Dimensions="${dim}" Format="${fmt}" Coverage="${cov}" → ${tag}`);
+    } else if (i % 50 === 0) {
+      process.stdout.write(`\r  ${i}/${rows.length}  found=${found.length}  no-width=${noWidth.length}  httpErr=${httpErr}`);
+    }
+    await sleep(VALIDATE ? 250 : 1000);   // politeness
+  }
+  console.log(`\n\n=== Newwall width re-crawl ${VALIDATE ? '(VALIDATE)' : COMMIT ? '(COMMIT)' : '(DRY-RUN)'} ===`);
+  console.log(`  crawled          : ${rows.length}`);
+  console.log(`  width found      : ${found.length}`);
+  console.log(`  no width (skip)  : ${noWidth.length}  (mural Custom / no spec / http err — left DRAFT, never invented)`);
+  console.log(`  http errors      : ${httpErr}`);
+  // breakdown of skip reasons
+  const reasons = {}; for (const n of noWidth) { const k = n.reason.split('(')[0].split(':')[0]; reasons[k] = (reasons[k]||0)+1; }
+  console.log(`  skip reasons     : ${JSON.stringify(reasons)}`);
+
+  // write audit snapshot + change manifest
+  const auditDir = path.join(__dirname, 'data', 'newwall-width-recrawl');
+  fs.mkdirSync(auditDir, { recursive: true });
+  const TS = new Date().toISOString().replace(/[:.]/g, '-');
+  const suffix = VALIDATE ? 'validate' : COMMIT ? 'applied' : 'dryrun';
+  fs.writeFileSync(path.join(auditDir, `found-${TS}.${suffix}.jsonl`), found.map(r => JSON.stringify(r)).join('\n') + (found.length ? '\n' : ''));
+  fs.writeFileSync(path.join(auditDir, `skipped-${TS}.${suffix}.jsonl`), noWidth.map(r => JSON.stringify(r)).join('\n') + (noWidth.length ? '\n' : ''));
+  console.log(`  manifests        → ${auditDir}/{found,skipped}-${TS}.${suffix}.jsonl`);
+
+  if (VALIDATE || !COMMIT) {
+    if (!VALIDATE) console.log('\nDRY-RUN — re-run with --commit to write width + width_inches to Mac2-local newwall_catalog.');
+    process.exit(0);
+  }
+
+  // COMMIT: snapshot prior values for the touched dw_skus, then batched UPDATE (reversible)
+  console.log('\napplying width + width_inches to Mac2-local newwall_catalog…');
+  const ids = found.map(r => `'${r.dw_sku.replace(/'/g, "''")}'`).join(',');
+  const before = psql(`SELECT dw_sku, coalesce(width,''), coalesce(width_inches::text,'') FROM newwall_catalog WHERE dw_sku IN (${ids});`);
+  fs.writeFileSync(path.join(auditDir, `before-${TS}.tsv`), before + '\n');
+  console.log(`  before-snapshot  → ${auditDir}/before-${TS}.tsv (reversal source)`);
+
+  let applied = 0;
+  for (let j = 0; j < found.length; j += 400) {
+    const batch = found.slice(j, j + 400);
+    const wCases = batch.map(r => `WHEN '${r.dw_sku.replace(/'/g, "''")}' THEN '${r.canonical.replace(/'/g, "''")}'`).join(' ');
+    const wiCases = batch.map(r => `WHEN '${r.dw_sku.replace(/'/g, "''")}' THEN ${r.width_in}`).join(' ');
+    const bIds = batch.map(r => `'${r.dw_sku.replace(/'/g, "''")}'`).join(',');
+    psql(`UPDATE newwall_catalog
+            SET width = CASE dw_sku ${wCases} END,
+                width_inches = CASE dw_sku ${wiCases} END,
+                updated_at = now()
+          WHERE dw_sku IN (${bIds});`);
+    applied += batch.length;
+    process.stdout.write(`\r  updated ${applied}/${found.length}…`);
+  }
+  console.log(`\napplied ${applied} width backfills. Reverse from before-${TS}.tsv. Next: node normalize-widths.js --only Newwall --commit`);
+})().catch(e => { console.error('ERR:', e.message); process.exit(1); });

← d3a6b2c6 Relabel 8 Brewster & York drafts -> Malibu Wallpaper + promo  ·  back to Designer Wallcoverings  ·  active-gate-add-samples: additive sample-variant pass for th 1c17d27c →