← back to Pattern Vault
scripts/build-lead-collection.mjs
91 lines
#!/usr/bin/env node
// Build a curated "lead collection" manifest of the 14 in-house ORIGINAL designs for the
// patterndesignlab.com licensing marketplace to seed page 1 from. These are the credible
// money-lane leaders (heritage botanical / dark moody floral / chinoiserie / deco) — NOT the
// novelty drunk-animals content both contrarian passes flagged for curation.
//
// Joins: data/catalog.json (in-house rows) + kits/etsy/listings.csv (AI-disclosed sell-copy).
// Output: data/lead-collection.json — consumed by the PDL builder (handoff via inter-session inbox).
//
// Usage: node scripts/build-lead-collection.mjs
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/catalog.json'), 'utf8'));
const inhouse = catalog.filter((d) => d.category === 'in-house-original');
// --- pull AI-disclosed descriptions from the etsy kit CSV, keyed by image filename ---
function parseCsv(s) {
const rows = []; let f = [], cur = '', inq = false;
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (inq) { if (c === '"') { if (s[i + 1] === '"') { cur += '"'; i++; } else inq = false; } else cur += c; }
else if (c === '"') inq = true;
else if (c === ',') { f.push(cur); cur = ''; }
else if (c === '\n') { f.push(cur); rows.push(f); f = []; cur = ''; }
else if (c !== '\r') cur += c;
}
if (cur || f.length) { f.push(cur); rows.push(f); }
return rows.filter((r) => r.length > 1 || r[0] !== '');
}
const csv = parseCsv(fs.readFileSync(path.join(ROOT, 'kits/etsy/listings.csv'), 'utf8'));
const h = csv[0];
const byImg = new Map();
for (const r of csv.slice(1)) {
const img = r[h.indexOf('image')]; // _images/ih-<slug>.png
byImg.set(path.basename(img).replace(/\.png$/, ''), { desc: r[h.indexOf('description')], tags: (r[h.indexOf('tags')] || '').split(',').map((t) => t.trim()).filter(Boolean) });
}
const slugOf = (d) => 'ih-' + path.basename(d.img).replace(/\.[a-z]+$/i, '');
// --- money-lane classification + page-1 rank (higher score = earlier on page 1) ---
const LANES = [
{ lane: 'Dark Moody Floral', rank: 100, re: /nocturne|moody|dark.*floral|crimson|amethyst|magnolia|camellia/i },
{ lane: 'Heritage Botanical', rank: 95, re: /heritage|botanical|trellis|arden|vine|rosewater|calico|indigo/i },
{ lane: 'Chinoiserie', rank: 85, re: /chinoiserie|gilded|bough/i },
{ lane: 'Painterly Floral', rank: 80, re: /painterly|cottage|bloom|floral/i },
{ lane: 'Art Deco', rank: 70, re: /deco|gatsby|geometric.*lux/i },
{ lane: 'Mid-Century', rank: 55, re: /mid-century|atomic|retro/i },
{ lane: 'Coastal', rank: 50, re: /coastal|tide|stripe/i },
{ lane: 'Folk / Block-Print', rank: 45, re: /folk|block|tile/i },
];
const classify = (d) => {
const blob = `${d.title} ${d.styleLine}`;
for (const L of LANES) if (L.re.test(blob)) return L;
return { lane: 'Original', rank: 30 };
};
const items = inhouse.map((d) => {
const kit = byImg.get(slugOf(d)) || {};
const L = classify(d);
return {
id: d.id,
title: d.title,
lane: L.lane,
leadRank: L.rank,
preview: d.img, // /assets/pilot/*.webp (web-res; PDL should copy or reference)
roomImg: d.roomImg || null,
dominantHex: d.dominantHex && d.dominantHex !== 'None' ? d.dominantHex : null,
description: kit.desc || d.styleLine, // AI-disclosed sell-copy
tags: kit.tags || [],
licenseTiers: d.licenseTiers, // $149 / $495 / $2500
masterStatus: 'kamatera-gated-pull (wallco-tif) — deliver only after real 150-DPI master exists',
provenance: d.provenance,
};
}).sort((a, b) => b.leadRank - a.leadRank);
const out = {
_for: 'patterndesignlab.com licensing marketplace — page-1 lead collection',
_from: 'pattern-vault session (session-pattern-vault-kits-260704)',
_note: 'The credible money-lane in-house ORIGINALS. AI-disclosed sell-copy included. Do NOT lead with novelty drunk-animals content (curate those out). Deliver licenses only against real 150-DPI Kamatera masters.',
generatedFor: 'PDL page-1 seed',
count: items.length,
lanes: [...new Set(items.map((i) => i.lane))],
items,
};
fs.writeFileSync(path.join(ROOT, 'data/lead-collection.json'), JSON.stringify(out, null, 2));
console.log(`lead-collection.json — ${items.length} originals across ${out.lanes.length} lanes`);
items.forEach((i) => console.log(` ${String(i.leadRank).padStart(3)} · ${i.lane.padEnd(20)} · ${i.title}`));