← back to Dw Collections Viewer

scripts/fill-gaps.js

94 lines

// One-shot script: for each "gap" slug whose auto-seed score was too low,
// look up the best DW Shopify collection by hand-curated keyword and assign it.
// Writes back to ~/Projects/_dw-batch/curation-rules.json.

const fs = require('fs');
const path = require('path');
const os = require('os');

const HOME = os.homedir();
const RULES_PATH = path.join(HOME, 'Projects', '_dw-batch', 'curation-rules.json');
const CACHE_DIR = path.join(__dirname, '..', 'data');

// hand-curated keyword overrides for the 21 gaps
const KEYWORDS = {
  raffiawallcoverings:    ['raffia'],
  raffiawalls:            ['raffia'],
  textilewallpaper:       ['textile', 'textile-wallcovering'],
  silverleafwallpaper:    ['silver-leaf', 'silver-foil', 'silver'],
  embroideredwallpaper:   ['embroidered', 'embroidery'],
  '1800swallpaper':       ['victorian', '19th-century', 'arts-and-crafts'],
  '1890swallpaper':       ['victorian', 'aesthetic-movement', 'arts-and-crafts'],
  '1900swallpaper':       ['edwardian', 'art-nouveau', 'arts-and-crafts'],
  '1920swallpaper':       ['art-deco', 'deco', 'jazz-age'],
  '1930swallpaper':       ['art-deco', 'moderne', 'streamlined'],
  '1980swallpaper':       ['memphis', '80s', 'post-modern'],
  wallpapersback:         ['wallpaper-collection', 'wallpapers'],
  agedwallpaper:          ['aged', 'patina', 'weathered', 'distressed'],
  handcraftedwallpaper:   ['hand-printed', 'hand-blocked', 'block-print', 'handcrafted', 'artisan'],
  museumwallpaper:        ['museum', 'gallery', 'institutional'],
  pastelwallpaper:        ['pastel'],
  recycledwallpaper:      ['recycled', 'sustainable', 'eco', 'natural'],
  hotelwallcoverings:     ['hotel', 'hospitality'],
  healthcarewallpaper:    ['healthcare', 'medical', 'antimicrobial'],
  restaurantwallpaper:    ['restaurant', 'dining'],
  saloonwallpaper:        ['western', 'saloon', 'old-west'],
};

function loadCollections() {
  const all = [];
  for (let p = 1; p <= 6; p++) {
    const f = path.join(CACHE_DIR, `collections-page-${p}.json`);
    if (!fs.existsSync(f)) break;
    try {
      const d = JSON.parse(fs.readFileSync(f, 'utf8'));
      all.push(...(d.collections || []));
    } catch (e) {}
  }
  return all;
}

function findBest(keywords, all) {
  // Score: exact handle match = 10; handle starts/ends with kw = 6; handle.includes = 4; title match = 2
  // Prefer collections with ≥30 products (skip tiny ones)
  let best = null, bestScore = -1;
  for (const c of all) {
    if ((c.products_count || 0) < 30) continue;
    const handle = (c.handle || '').toLowerCase();
    const title = (c.title || '').toLowerCase();
    let s = 0;
    for (const kw of keywords) {
      if (handle === kw) s += 10;
      else if (handle.startsWith(kw + '-') || handle.endsWith('-' + kw)) s += 6;
      else if (handle.includes(kw)) s += 4;
      if (title.includes(kw)) s += 2;
    }
    if (s > bestScore) { bestScore = s; best = c; }
  }
  return bestScore >= 4 ? { ...best, _score: bestScore } : null;
}

const all = loadCollections();
console.log(`loaded ${all.length} collections from cache`);
const rules = JSON.parse(fs.readFileSync(RULES_PATH, 'utf8'));
const filled = [], stillGap = [];
for (const [slug, kws] of Object.entries(KEYWORDS)) {
  if (!rules.sites[slug]) { stillGap.push({ slug, reason: 'no rule' }); continue; }
  const existing = Array.isArray(rules.sites[slug].shopify_collections) ? rules.sites[slug].shopify_collections : [];
  if (existing.length > 0) { stillGap.push({ slug, reason: 'already has ' + existing.length }); continue; }
  const m = findBest(kws, all);
  if (m) {
    rules.sites[slug].shopify_collections = [m.handle];
    filled.push({ slug, handle: m.handle, title: m.title, products: m.products_count, score: m._score });
  } else {
    stillGap.push({ slug, reason: 'no match' });
  }
}

fs.writeFileSync(RULES_PATH, JSON.stringify(rules, null, 2));

console.log(`\nFILLED ${filled.length}:`);
for (const f of filled) console.log(`  ${f.slug.padEnd(28)} -> ${f.handle.padEnd(36)} (${f.products} prod, score ${f.score}) · ${f.title.slice(0, 36)}`);
console.log(`\nSTILL GAP ${stillGap.length}:`);
for (const g of stillGap) console.log(`  ${g.slug.padEnd(28)} ${g.reason}`);