[object Object]

← back to Reidwitlin Landing

auto-save: 2026-07-07T12:30:21 (4 files) — lib/backfill-source-images.js lib/relabel-colorways.js scripts/backfill-source-images.js scripts/build-rwltd-data.js

b8b384a7cb37a63759684a41726f200a64f23abb · 2026-07-07 12:30:22 -0700 · Steve Abrams

Files touched

Diff

commit b8b384a7cb37a63759684a41726f200a64f23abb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 7 12:30:22 2026 -0700

    auto-save: 2026-07-07T12:30:21 (4 files) — lib/backfill-source-images.js lib/relabel-colorways.js scripts/backfill-source-images.js scripts/build-rwltd-data.js
---
 lib/backfill-source-images.js     | 27 +++++++++++++++++++--------
 lib/relabel-colorways.js          |  2 +-
 scripts/backfill-source-images.js | 14 +++++++-------
 scripts/build-rwltd-data.js       |  4 ++--
 4 files changed, 29 insertions(+), 18 deletions(-)

diff --git a/lib/backfill-source-images.js b/lib/backfill-source-images.js
index 1756b57..077086f 100644
--- a/lib/backfill-source-images.js
+++ b/lib/backfill-source-images.js
@@ -15,6 +15,8 @@
  */
 const https = require('https');
 
+const MAX_PAGES = 6; // rwltd.com typically 3–5 pages; 6 is a safe ceiling
+
 // MUST match build-rwltd-data.js's slug: drop apostrophes BEFORE the char-class
 // collapse so "I'm Crushed" -> "im-crushed" (matching the vendor mfr_sku), not
 // "i-m-crushed". A divergence here silently breaks the feed-key<->sku lookup for
@@ -27,7 +29,9 @@ function get(url) {
   return new Promise((res, rej) => {
     https.get(url, opts, r => {
       if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location) return res(get(r.headers.location));
-      let d = ''; r.on('data', c => d += c); r.on('end', () => res(d));
+      let d = '';
+      r.on('data', c => { d += c; });
+      r.on('end', () => res(d));
     }).on('error', rej);
   });
 }
@@ -35,16 +39,18 @@ function get(url) {
 // Pull the full source feed and build slug("pattern-option") -> featured_image src.
 async function fetchSourceImageIndex(base = 'https://rwltd.com/products.json?limit=250') {
   const feed = [];
-  for (let page = 1; page <= 6; page++) {
+  for (let page = 1; page <= MAX_PAGES; page++) {
     let ps = [];
     try { ps = JSON.parse(await get(`${base}&page=${page}`)).products || []; } catch { break; }
     if (!ps.length) break;
     feed.push(...ps);
   }
   const idx = {};
-  for (const p of feed) for (const v of (p.variants || [])) {
-    const fi = v.featured_image && v.featured_image.src;
-    if (fi) idx[slug(`${p.title}-${v.option1}`)] = fi;
+  for (const p of feed) {
+    for (const v of (p.variants || [])) {
+      const fi = v.featured_image?.src;
+      if (fi) idx[slug(`${p.title}-${v.option1}`)] = fi;
+    }
   }
   return { idx, patterns: feed.length };
 }
@@ -58,16 +64,21 @@ async function fetchSourceImageIndex(base = 'https://rwltd.com/products.json?lim
  */
 async function backfillFromSourceFeed(products, opts = {}) {
   const { idx, patterns } = await fetchSourceImageIndex(opts.base);
-  let backfilled = 0; const conflicts = [];
+  let backfilled = 0;
+  const conflicts = [];
   for (const p of products) {
     const src = idx[slug(p.sku)];
     if (!src) continue;
     const noPhoto = p.image_note || !(p.images && p.images.length);
     if (noPhoto) {
-      p.images = [src]; p.swatch = src; p.room = src; delete p.image_note;
+      p.images = [src];
+      p.swatch = src;
+      p.room = src;
+      delete p.image_note;
       p.image_source = 'rwltd-variant'; backfilled++;
     } else {
-      const cur = (p.swatch || '').split('?')[0], s0 = src.split('?')[0];
+      const cur = (p.swatch || '').split('?')[0];
+      const s0 = src.split('?')[0];
       if (cur !== s0) conflicts.push({ handle: p.handle, color: p.color, ours: cur.split('/').pop(), source: s0.split('/').pop() });
     }
   }
diff --git a/lib/relabel-colorways.js b/lib/relabel-colorways.js
index 94e736b..1bd7c9f 100644
--- a/lib/relabel-colorways.js
+++ b/lib/relabel-colorways.js
@@ -48,7 +48,7 @@ function relabelUnreliableSeries(products, { MIN_N = 4, MIN_HUES = 4 } = {}) {
 
   for (const [series, ps] of bySeries) {
     const labels = new Set(ps.map(p => norm(p.color)).filter(Boolean));
-    const hexes = new Set(ps.map(p => (p.hex || '').toUpperCase()).filter(Boolean));
+    const hexes = new Set(ps.map(p => p.hex?.toUpperCase()).filter(Boolean));
     const unreliable = ps.length >= MIN_N && labels.size === 1 && hexes.size >= MIN_HUES;
     if (!unreliable) continue;
 
diff --git a/scripts/backfill-source-images.js b/scripts/backfill-source-images.js
index 8844ce7..5f158de 100644
--- a/scripts/backfill-source-images.js
+++ b/scripts/backfill-source-images.js
@@ -36,17 +36,17 @@ const BUCKET_ORDER = ['white','grey','black','pink','red','orange','brown','gold
   snap.drafted_no_own_photo = snap.drafts.length;
 
   // Recompute facets from the NEW published set (the partition changed which are live).
-  const fb = {}, fsr = {}, fc = {};
+  const bookCounts = {}, seriesCounts = {}, colorCounts = {};
   for (const p of snap.products) {
-    if (p.book) fb[p.book] = (fb[p.book] || 0) + 1;
-    if (p.series) fsr[p.series] = (fsr[p.series] || 0) + 1;
-    if (p.color_bucket) fc[p.color_bucket] = (fc[p.color_bucket] || 0) + 1;
+    if (p.book) bookCounts[p.book] = (bookCounts[p.book] || 0) + 1;
+    if (p.series) seriesCounts[p.series] = (seriesCounts[p.series] || 0) + 1;
+    if (p.color_bucket) colorCounts[p.color_bucket] = (colorCounts[p.color_bucket] || 0) + 1;
   }
   snap.facets = {
     total: snap.products.length,
-    books: Object.entries(fb).sort((a, b) => b[1] - a[1]),
-    series: Object.entries(fsr).sort((a, b) => b[1] - a[1]).slice(0, 300),
-    colors: BUCKET_ORDER.filter(b => fc[b]).map(b => [b, fc[b]]),
+    books: Object.entries(bookCounts).sort((a, b) => b[1] - a[1]),
+    series: Object.entries(seriesCounts).sort((a, b) => b[1] - a[1]).slice(0, 300),
+    colors: BUCKET_ORDER.filter(b => colorCounts[b]).map(b => [b, colorCounts[b]]),
   };
 
   console.log(`backfilled ${backfilled} colorways | published ${snap.products.length} | drafted ${snap.drafts.length} | facets.total ${snap.facets.total}`);
diff --git a/scripts/build-rwltd-data.js b/scripts/build-rwltd-data.js
index 40bf589..9bb46ce 100644
--- a/scripts/build-rwltd-data.js
+++ b/scripts/build-rwltd-data.js
@@ -327,8 +327,8 @@ async function main() {
   // label lies. Relabel from the pixel-derived colour family so each card's text
   // matches its image. Handles/SKUs untouched — links still hit the same product.
   const { relabelUnreliableSeries } = require('../lib/relabel-colorways');
-  const rl = relabelUnreliableSeries(products);
-  if (rl.productsFixed) console.log(`  colorway labels: relabelled ${rl.productsFixed} in ${rl.seriesFixed} mis-stamped series (${rl.series.join(', ')})`);
+  const relabel = relabelUnreliableSeries(products);
+  if (relabel.productsFixed) console.log(`  colorway labels: relabelled ${relabel.productsFixed} in ${relabel.seriesFixed} mis-stamped series (${relabel.series.join(', ')})`);
 
   // Source-image backfill — a FOURTH pass, BEFORE the own-photo partition. The
   // scrape lost the per-colorway image mapping; the free rwltd.com Shopify feed

← d67eec9 reidwitlin: contrarian FIX-THEN-SHIP — guard silent-zero-res  ·  back to Reidwitlin Landing  ·  chore: lint, refactor, v0.2.3 (session close) d7714e5 →