← back to Feature Harvest

build-corpus.js

223 lines

#!/usr/bin/env node
/*
 * build-corpus.js — aggregate a year of work into one reviewable list (data/items.json).
 * Sources:
 *   A) CNCP wins             → shipped features/apps           (source: "win")
 *   B) DW-Programming/*       → standalone DW apps/tools        (source: "dw-app")
 *   C) wallco/WPB curators    → AI-storefront review tools      (source: "wpb-tool")
 * Each item: { id, source, title, project, date, summary, value, commit, path, port, hasUI }
 */
const fs = require('fs');
const path = require('path');
const os = require('os');

const HOME = os.homedir();
const OUT = path.join(__dirname, 'data', 'items.json');
const items = [];

// ---- A) CNCP wins -------------------------------------------------
function addWins() {
  const snap = path.join(__dirname, 'data', 'wins-snapshot.json');
  let wins = [];
  try { wins = JSON.parse(fs.readFileSync(snap, 'utf8')); } catch {}
  for (const w of wins) {
    items.push({
      id: w.id,
      source: 'win',
      title: w.title,
      project: w.project || '',
      date: w.date || (w.createdAt ? new Date(w.createdAt).toISOString().slice(0, 10) : ''),
      ts: w.createdAt || 0,
      summary: w.summary || '',
      value: w.valueToday || '',
      commit: w.commit || '',
      path: '',
      port: '',
      hasUI: false,
    });
  }
  return wins.length;
}

// ---- helpers for filesystem apps ----------------------------------
function firstReadmeLine(dir) {
  for (const name of ['README.md', 'readme.md', 'VERSION_NOTES.md', 'VERSION.md']) {
    const f = path.join(dir, name);
    if (fs.existsSync(f)) {
      const lines = fs.readFileSync(f, 'utf8').split('\n').map((l) => l.trim());
      // first non-empty, non-heading-only line, else first heading text
      const para = lines.find((l) => l && !l.startsWith('#') && !l.startsWith('---'));
      const head = lines.find((l) => l.startsWith('#'));
      const txt = (para || (head ? head.replace(/^#+\s*/, '') : '') || '').slice(0, 400);
      if (txt) return txt;
    }
  }
  return '';
}
function pkgDesc(dir) {
  const f = path.join(dir, 'package.json');
  if (fs.existsSync(f)) {
    try { return (JSON.parse(fs.readFileSync(f, 'utf8')).description || '').slice(0, 400); } catch {}
  }
  return '';
}
function detectPort(dir) {
  for (const name of ['ecosystem.config.js', '.env', 'server.js', 'index.js']) {
    const f = path.join(dir, name);
    if (fs.existsSync(f)) {
      try {
        const m = fs.readFileSync(f, 'utf8').match(/(?:PORT|port)\s*[:=]\s*['"]?(\d{3,5})/);
        if (m) return m[1];
      } catch {}
    }
  }
  return '';
}
function hasUI(dir) {
  if (fs.existsSync(path.join(dir, 'public', 'index.html'))) return true;
  try { return fs.readdirSync(dir).some((f) => f.endsWith('.html')); } catch { return false; }
}
function dirDate(dir) {
  try {
    const t = fs.statSync(dir).mtime;
    return { date: t.toISOString().slice(0, 10), ts: t.getTime() };
  } catch { return { date: '', ts: 0 }; }
}

// ---- B) DW-Programming app folders --------------------------------
const SKIP_DIRS = new Set([
  'config', 'data', 'lib', 'ls', 'scripts', 'state', 'templates',
  'shared-enhancements', 'node_modules', '.git',
]);
function addDirApps(parent, source, projectLabel) {
  if (!fs.existsSync(parent)) return 0;
  let n = 0;
  for (const name of fs.readdirSync(parent)) {
    if (SKIP_DIRS.has(name) || name.startsWith('.')) continue;
    const dir = path.join(parent, name);
    let st; try { st = fs.statSync(dir); } catch { continue; }
    if (!st.isDirectory()) continue;
    const { date, ts } = dirDate(dir);
    const summary = firstReadmeLine(dir) || pkgDesc(dir) || '';
    items.push({
      id: `${source}:${name}`,
      source,
      title: name,
      project: projectLabel,
      date, ts,
      summary,
      value: '',
      commit: '',
      path: dir,
      port: detectPort(dir),
      hasUI: hasUI(dir),
    });
    n++;
  }
  return n;
}

// ---- C) Standalone curator/viewer HTML tools (wallco + WPB) -------
function addHtmlTools() {
  const roots = [
    { dir: path.join(HOME, 'Projects/wallco-ai/public'), source: 'wpb-tool', label: 'wallco-ai' },
    { dir: path.join(HOME, 'Projects/WallpapersBack/wallpapersback-fork/public'), source: 'wpb-tool', label: 'wallpapersback' },
  ];
  let n = 0;
  for (const { dir, source, label } of roots) {
    if (!fs.existsSync(dir)) continue;
    for (const f of fs.readdirSync(dir)) {
      if (!f.endsWith('.html')) continue;
      if (['index.html', 'privacy.html', 'about.html'].includes(f)) continue;
      const full = path.join(dir, f);
      const { date, ts } = dirDate(full);
      items.push({
        id: `${source}:${label}:${f}`,
        source,
        title: f.replace(/\.html$/, '').replace(/[-_]/g, ' '),
        project: label,
        date, ts,
        summary: `Standalone review/curator tool (${f}) in the ${label} storefront.`,
        value: '',
        commit: '',
        path: full,
        port: '',
        hasUI: true,
      });
      n++;
    }
  }
  return n;
}

// ---- D) skills + E) agents (the "forgotten tool" review) ----------
function frontmatter(file) {
  try {
    const t = fs.readFileSync(file, 'utf8');
    const m = t.match(/^---\n([\s\S]*?)\n---/);
    if (!m) return {};
    const fm = {};
    const nm = m[1].match(/^name:\s*(.+)$/m); if (nm) fm.name = nm[1].trim();
    const ds = m[1].match(/^description:\s*([\s\S]+?)(?:\n[a-z_]+:|\n*$)/m);
    if (ds) fm.description = ds[1].replace(/\s+/g, ' ').trim();
    return fm;
  } catch { return {}; }
}
function addSkills() {
  const root = path.join(HOME, '.claude/skills');
  if (!fs.existsSync(root)) return 0;
  let n = 0;
  for (const name of fs.readdirSync(root)) {
    const sk = path.join(root, name, 'SKILL.md');
    if (!fs.existsSync(sk)) continue;
    let st; try { st = fs.statSync(sk); } catch { continue; }
    const fm = frontmatter(sk);
    items.push({
      id: `skill:${name}`, source: 'skill', title: fm.name || name, project: 'skills',
      date: st.mtime.toISOString().slice(0, 10), ts: st.mtime.getTime(),
      summary: (fm.description || '').slice(0, 500), value: '', commit: '',
      path: sk, port: '', hasUI: false,
    });
    n++;
  }
  return n;
}
function addAgents() {
  const root = path.join(HOME, '.claude/agents');
  if (!fs.existsSync(root)) return 0;
  let n = 0;
  for (const f of fs.readdirSync(root)) {
    if (!f.endsWith('.md')) continue;
    const fp = path.join(root, f);
    let st; try { st = fs.statSync(fp); if (!st.isFile()) continue; } catch { continue; }
    const fm = frontmatter(fp);
    items.push({
      id: `agent:${f.replace(/\.md$/, '')}`, source: 'agent',
      title: fm.name || f.replace(/\.md$/, ''), project: 'agents',
      date: st.mtime.toISOString().slice(0, 10), ts: st.mtime.getTime(),
      summary: (fm.description || '').slice(0, 500), value: '', commit: '',
      path: fp, port: '', hasUI: false,
    });
    n++;
  }
  return n;
}

// ---- run ----------------------------------------------------------
const nWins = addWins();
const nDW = addDirApps(path.join(HOME, 'Projects/Designer-Wallcoverings/DW-Programming'), 'dw-app', 'DW-Programming');
const nTools = addHtmlTools();
const nSkills = addSkills();
const nAgents = addAgents();

// stable sort: newest first
items.sort((a, b) => (b.ts || 0) - (a.ts || 0));
fs.writeFileSync(OUT, JSON.stringify(items, null, 2));
console.log(`corpus built → ${OUT}`);
console.log(`  wins:     ${nWins}`);
console.log(`  dw-apps:  ${nDW}`);
console.log(`  wpb-tools:${nTools}`);
console.log(`  skills:   ${nSkills}`);
console.log(`  agents:   ${nAgents}`);
console.log(`  TOTAL:    ${items.length}`);