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