← back to New Arrivals Content Engine

scripts/draft-social.cjs

101 lines

#!/usr/bin/env node
/**
 * New-Arrivals Content Engine — STEP 2: draft social posts  (LOCAL, $0, NO POST).
 *
 * Reads data/net-new-latest.json (from diff-new-arrivals.cjs), picks the top
 * PICK net-new products (prefers ones WITH a featured image), and writes a
 * DRAFT post package per product into data/review-queue/ — one JSON file each,
 * status:"draft". Captions are generated locally from tag-derived style/color
 * vocabulary (no paid API, no LLM call), and are Wallcovering-safe (no medical/
 * performance/superlative claims that trip the outbound-comms gate).
 *
 * HARD RAIL: this NEVER posts to any platform and never schedules a live send.
 * Publishing is Steve-gated. The room-render/reels step is left as an OPTIONAL
 * $0-local hook (render_hint) the reviewer can trigger via reels-producer.
 *
 * Usage: node draft-social.cjs [--pick 5]
 */
const fs = require('fs');
const path = require('path');

const args = process.argv.slice(2);
const PICK = parseInt(args.find((_, i, a) => a[i - 1] === '--pick') || '5', 10) || 5;

const ROOT = path.join(__dirname, '..');
const IN = path.join(ROOT, 'data', 'net-new-latest.json');
const QUEUE = path.join(ROOT, 'data', 'review-queue');
const LOG = path.join(ROOT, 'data', 'engine.log');

if (!fs.existsSync(IN)) { console.error('Run diff-new-arrivals.cjs first — no net-new-latest.json'); process.exit(1); }
const data = JSON.parse(fs.readFileSync(IN, 'utf8'));
const stamp = () => new Date().toISOString().replace('T', ' ').slice(0, 19);
const log = (m) => { const l = `[${stamp()}] ${m}`; console.log(l); try { fs.appendFileSync(LOG, l + '\n'); } catch (_) {} };

// --- local vocab: derive a style + color read from tags/title (deterministic) ---
const STYLE_WORDS = ['Traditional', 'Modern', 'Floral', 'Damask', 'Geometric', 'Grasscloth',
  'Botanical', 'Stripe', 'Toile', 'Chinoiserie', 'Textured', 'Metallic', 'Abstract'];
const COLOR_WORDS = ['Alabaster', 'Oatmeal', 'Greige', 'Celadon', 'Indigo', 'Charcoal', 'Blush',
  'Emerald', 'Navy', 'Gold', 'Ivory', 'Sage', 'Terracotta', 'Slate', 'Cream'];

function pickWord(list, hay) {
  const h = hay.toLowerCase();
  return list.find((w) => h.includes(w.toLowerCase())) || null;
}

function caption(p) {
  const hay = `${p.title} ${p.tags.join(' ')} ${p.type}`;
  const style = pickWord(STYLE_WORDS, hay);
  const color = pickWord(COLOR_WORDS, hay);
  const kind = (p.type || 'Wallcovering').replace(/s$/, '');
  const lead = `New arrival: ${p.title}`;
  const desc = [
    style ? `${style.toLowerCase()} ${kind.toLowerCase()}` : `${kind.toLowerCase()}`,
    color ? `in a ${color.toLowerCase()} palette` : null,
  ].filter(Boolean).join(' ');
  const base = `${lead} — a ${desc}, just added at Designer Wallcoverings.`;
  const tagset = ['#wallcovering', '#interiordesign', '#designerwallcoverings',
    style ? `#${style.toLowerCase()}` : null, '#newarrival'].filter(Boolean);
  return {
    instagram: `${base}\nSwatch samples available. Link in bio.\n${tagset.join(' ')}`,
    tiktok: `${lead} ✨ ${desc}. #wallcovering #interiordesign #fyp`,
    pinterest: { title: `${p.title} — ${style || kind}`, description: base },
    derived: { style, color, kind },
  };
}

function slug(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60); }

// prefer products with an image; keep created order otherwise
const ranked = [...data.net_new].sort((a, b) => (b.image ? 1 : 0) - (a.image ? 1 : 0));
const picks = ranked.slice(0, PICK);

fs.mkdirSync(QUEUE, { recursive: true });
let written = 0;
for (const p of picks) {
  const cap = caption(p);
  const numericId = String(p.id).split('/').pop();
  const draft = {
    status: 'draft',                 // NEVER auto-posts; reviewer flips to approved
    created: stamp(),
    date: data.date,
    product: { id: p.id, title: p.title, handle: p.handle, vendor: p.vendor, type: p.type, image: p.image },
    product_url: `https://www.designerwallcoverings.com/products/${p.handle}`,
    captions: cap,
    platforms: ['instagram', 'tiktok', 'pinterest'],
    asset: {
      featured_image: p.image,
      render_hint: p.image
        ? `reels-producer: $0 local slideshow/room-render from ${p.image}`
        : 'no featured image — needs asset before publish',
      render_status: 'pending',      // reviewer triggers reels-producer if wanted
    },
    gate: 'PUBLISH IS STEVE-GATED — this file is a DRAFT only. No send performed.',
  };
  const fp = path.join(QUEUE, `${data.date}__${slug(p.title || numericId)}.json`);
  fs.writeFileSync(fp, JSON.stringify(draft, null, 2));
  written++;
}

log(`drafted ${written} DRAFT post packages into ${QUEUE} (net-new pool=${data.net_new_count}, no posts sent)`);
console.log(`\n${written} draft(s) written to data/review-queue/ — review, then approve to publish (gated).`);