← back to Council 0809 Builds

sourcing-brief/build-brief.js

91 lines

#!/usr/bin/env node
/**
 * Design Trend Scout -> Weekly Sourcing Brief  (Council idea #3, 2026-08-09)
 * -------------------------------------------------------------------------
 * design-trend-scout already finds catalog gaps vs Spoonflower/Etsy top
 * sellers, but its findings are ephemeral. This turns them into a standing
 * Monday HTML artifact Steve can act on: top-N trend gaps, each with the
 * marketplace evidence, the DW gap, and a suggested (settlement-safe) design
 * brief.
 *
 * Input : a JSON array of gap objects (from design-trend-scout output), or the
 *         bundled sample if none supplied.
 * Output: an HTML brief to stdout (pipe to a file) — deliver via George.
 *
 * Zero paid spend. Reads local JSON, emits HTML. No network.
 *
 * Usage:
 *   node build-brief.js path/to/scout-gaps.json > brief.html
 *   node build-brief.js            # uses sample-gaps.json
 */
const fs = require('fs');
const path = require('path');

const esc = (s) =>
  String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));

function loadGaps(argPath) {
  const p = argPath || path.join(__dirname, 'sample-gaps.json');
  const gaps = JSON.parse(fs.readFileSync(p, 'utf8'));
  // rank: marketplace demand (evidence count / rank) x DW gap size
  return gaps
    .map((g) => ({ ...g, _score: (g.demand_score ?? 5) * (g.dw_gap_score ?? 5) }))
    .sort((a, b) => b._score - a._score);
}

function weekLabel() {
  // deterministic: caller passes WEEK env when scheduled; else 'this week'
  return process.env.WEEK || 'this week';
}

function render(gaps) {
  const rows = gaps
    .slice(0, Number(process.env.TOP || 5))
    .map(
      (g, i) => `
      <tr>
        <td class="rank">${i + 1}</td>
        <td>
          <div class="trend">${esc(g.trend)}</div>
          <div class="meta">${esc(g.style || '')} ${g.palette ? '· ' + esc(g.palette) : ''}</div>
        </td>
        <td>${esc(g.marketplace_evidence)}</td>
        <td>${esc(g.dw_gap)}</td>
        <td class="brief">${esc(g.design_brief)}
          ${g.settlement_note ? `<div class="warn">⚠ ${esc(g.settlement_note)}</div>` : ''}
        </td>
      </tr>`
    )
    .join('');

  return `<!doctype html><html><head><meta charset="utf-8">
<title>DW Sourcing Brief — ${esc(weekLabel())}</title>
<style>
  body{font:15px/1.5 -apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#1a1a1a;max-width:920px;margin:32px auto;padding:0 20px}
  h1{font-weight:600;letter-spacing:.3px;margin-bottom:2px}
  .sub{color:#777;margin-bottom:24px}
  table{border-collapse:collapse;width:100%}
  th,td{border-bottom:1px solid #e6e6e6;padding:12px 10px;vertical-align:top;text-align:left}
  th{font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:#999;font-weight:600}
  .rank{font-size:22px;color:#b7913f;font-weight:700;width:34px}
  .trend{font-weight:600}
  .meta{color:#888;font-size:13px}
  .brief{font-size:14px}
  .warn{color:#b00;font-size:12px;margin-top:6px}
  footer{margin-top:28px;color:#aaa;font-size:12px}
</style></head><body>
  <h1>Designer Wallcoverings — Weekly Sourcing Brief</h1>
  <div class="sub">Top trend-gaps vs Spoonflower / Etsy best-sellers · ${esc(weekLabel())} · generated from design-trend-scout</div>
  <table>
    <thead><tr><th>#</th><th>Trend</th><th>Marketplace evidence</th><th>DW gap</th><th>Suggested original design brief</th></tr></thead>
    <tbody>${rows}</tbody>
  </table>
  <footer>Every suggested brief must clear the settlement gate before generation. Company/vendor names are text-only. Original designs only — the scout identifies demand lanes, never copies a listing.</footer>
</body></html>`;
}

if (require.main === module) {
  process.stdout.write(render(loadGaps(process.argv[2])));
}
module.exports = { render, loadGaps };