← 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}`);