← back to Abrams

scripts/abrams-indexes.js

339 lines

#!/usr/bin/env node
// abrams-indexes.js — computes 8 proprietary Abrams indexes from live local data.
// Spiritual cousins to FEMA's Waffle House Index + the Pentagon Pizza Index — quirky,
// real, observable. Writes data/abrams-indexes.json with current values + 7-day
// sparklines + methodology blurbs.

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const ROOT = path.resolve(__dirname, '..');
const HOME = process.env.HOME;
const PROJECTS = path.join(HOME, 'Projects');
const OUT = path.join(ROOT, 'data', 'abrams-indexes.json');
const LOG = path.join(ROOT, 'data', 'build-log.jsonl');
const log = (type, payload) => fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), type, ...payload }) + '\n');

const exec = (cmd) => { try { return execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','ignore'], maxBuffer: 20 * 1024 * 1024 }).trim(); } catch { return ''; } };
const readJson = (p, fb) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fb; } };

// ── enumerate abrams projects on disk ─────────────────────────────────
function abramsDirs() {
  try {
    return fs.readdirSync(PROJECTS, { withFileTypes: true })
      .filter(d => d.isDirectory())
      .filter(d => /abrams/i.test(d.name))
      .map(d => path.join(PROJECTS, d.name));
  } catch { return []; }
}

// ── helper: commits per day, last 30 days, across all abrams projects ─
function commitTimeline(days = 30) {
  const since = Date.now() - days * 86400000;
  const dirs = abramsDirs();
  const buckets = {}; // ISO day → count
  for (const d of dirs) {
    if (!fs.existsSync(path.join(d, '.git'))) continue;
    const log = exec(`cd "${d}" && git log --since="${days} days ago" --format="%cI" 2>/dev/null`);
    log.split('\n').filter(Boolean).forEach(iso => {
      const day = iso.slice(0, 10);
      buckets[day] = (buckets[day] || 0) + 1;
    });
  }
  // dense series for the last `days` days
  const series = [];
  for (let i = days - 1; i >= 0; i--) {
    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
    series.push({ day, count: buckets[day] || 0 });
  }
  return series;
}

// ── helper: late-night commits (22:00-02:59 local) over N days ────────
function lateNightCommits(days = 30) {
  const dirs = abramsDirs();
  const all = [];
  for (const d of dirs) {
    if (!fs.existsSync(path.join(d, '.git'))) continue;
    const log = exec(`cd "${d}" && git log --since="${days} days ago" --format="%cI" 2>/dev/null`);
    log.split('\n').filter(Boolean).forEach(iso => {
      const h = new Date(iso).getHours();
      if (h >= 22 || h < 3) all.push(iso);
    });
  }
  return all;
}

// ── helper: pm2 status across the fleet ───────────────────────────────
function pm2Status() {
  try {
    const raw = exec('pm2 jlist 2>/dev/null');
    // pm2 jlist sometimes prints initialization noise on the first line; isolate the JSON array
    const start = raw.indexOf('[');
    const end = raw.lastIndexOf(']');
    if (start < 0 || end < 0) throw new Error('no JSON array');
    const ps = JSON.parse(raw.slice(start, end + 1));
    const all = ps.length;
    const online = ps.filter(p => p.pm2_env?.status === 'online').length;
    const restarting = ps.filter(p => (p.pm2_env?.unstable_restarts || 0) > 0).length;
    return { all, online, restarting, down: all - online };
  } catch { return { all: 0, online: 0, restarting: 0, down: 0 }; }
}

// ── helper: LOC across the empire ─────────────────────────────────────
function totalLoc() {
  const dirs = abramsDirs();
  let total = 0;
  for (const d of dirs) {
    const out = exec(`find "${d}" -type f \\( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.html" -o -name "*.css" -o -name "*.py" -o -name "*.sh" -o -name "*.md" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}'`);
    total += parseInt(out, 10) || 0;
  }
  return total;
}

// ── helper: sparkline generator (last 7 buckets of value) ─────────────
function spark7(timeline) {
  // timeline is array of {day, count}; take last 7 days
  return timeline.slice(-7).map(x => x.count);
}

// ── index 1 — Abrams Build Index (commits/day, normalized to 100 base on 7d avg) ──
function buildIndex() {
  const t30 = commitTimeline(30);
  const last7 = t30.slice(-7);
  const prev7 = t30.slice(-14, -7);
  const avg = a => a.length ? a.reduce((s,x)=>s+x.count,0)/a.length : 0;
  const cur = avg(last7);
  const prev = avg(prev7) || 1;
  return {
    id: 'ABI',
    name: 'Abrams Build Index',
    icon: '◐',
    value: Math.round((cur / prev) * 100),
    unit: 'idx',
    delta: Number((cur - avg(prev7)).toFixed(2)),
    delta_label: 'commits/day vs prev 7d',
    sparkline: spark7(t30),
    method: 'Daily commit count across all Abrams projects, indexed 100 = previous 7-day average. >100 = accelerating. <100 = cooling.',
    interpretation: cur > avg(prev7) ? 'BUILD ACCELERATING' : cur < avg(prev7) * 0.7 ? 'BUILD COOLING' : 'BUILD STEADY',
  };
}

// ── index 2 — Operator Heat Index (composite 0-100) ───────────────────
function operatorHeatIndex() {
  const t = commitTimeline(7);
  const today = t[t.length - 1]?.count || 0;
  const avg7 = t.reduce((s,x)=>s+x.count, 0) / 7;
  const pm2 = pm2Status();
  const pm2Score = pm2.all ? (pm2.online / pm2.all) * 50 : 0; // 50pt for full uptime
  const commitScore = Math.min(50, today * 5); // 50pt cap on commit velocity
  const value = Math.round(pm2Score + commitScore);
  return {
    id: 'OHI',
    name: 'Operator Heat Index',
    icon: '✸',
    value,
    unit: '/100',
    delta: today - Math.round(avg7),
    delta_label: 'commits today vs 7d avg',
    sparkline: spark7(t),
    method: 'Composite: 50pt for pm2 uptime ratio + 50pt cap for today commits × 5. Reads founder activity intensity.',
    interpretation: value > 70 ? 'HOT' : value > 40 ? 'WARM' : 'COLD',
  };
}

// ── index 3 — Late-Night Code Index (% of last-7-day commits in 22:00-02:59) ──
function lateNightIndex() {
  const dirs = abramsDirs();
  let totalRecent = 0, lateRecent = 0;
  const dailyLate = {};
  for (const d of dirs) {
    if (!fs.existsSync(path.join(d, '.git'))) continue;
    const log = exec(`cd "${d}" && git log --since="7 days ago" --format="%cI" 2>/dev/null`);
    log.split('\n').filter(Boolean).forEach(iso => {
      totalRecent++;
      const h = new Date(iso).getHours();
      if (h >= 22 || h < 3) {
        lateRecent++;
        const day = iso.slice(0, 10);
        dailyLate[day] = (dailyLate[day] || 0) + 1;
      }
    });
  }
  const pct = totalRecent ? Math.round((lateRecent / totalRecent) * 100) : 0;
  // build 7-day sparkline
  const series = [];
  for (let i = 6; i >= 0; i--) {
    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
    series.push(dailyLate[day] || 0);
  }
  return {
    id: 'LNCI',
    name: 'Late-Night Code Index',
    icon: '☾',
    value: pct,
    unit: '%',
    delta: lateRecent,
    delta_label: 'late-night commits in 7d',
    sparkline: series,
    method: '% of last-7-day commits landing between 22:00-02:59 local time. Productivity-vs-burnout signal — sustained >40% reads "founder is running hot."',
    interpretation: pct > 40 ? '🔥 BURNING THE MIDNIGHT OIL' : pct > 20 ? 'BALANCED' : 'DAYTIME OPERATOR',
  };
}

// ── index 4 — Abrams Pizza Index (count of late-night commits in 7d) ──
// (Spiritual cousin to the Pentagon Pizza Index — when commits spike late, something is shipping.)
function pizzaIndex() {
  const late = lateNightCommits(7);
  const value = late.length;
  // sparkline = late commits per day for last 7 days
  const daily = {};
  for (const iso of late) {
    const day = iso.slice(0, 10);
    daily[day] = (daily[day] || 0) + 1;
  }
  const series = [];
  for (let i = 6; i >= 0; i--) {
    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
    series.push(daily[day] || 0);
  }
  return {
    id: 'API',
    name: 'Abrams Pizza Index',
    icon: '🍕',
    value,
    unit: 'late-night commits / 7d',
    delta: series[series.length - 1] - (series[series.length - 2] || 0),
    delta_label: 'last 24h vs prior day',
    sparkline: series,
    method: 'Direct count of commits landing 22:00-02:59 local in the last 7 days. The original Pentagon Pizza Index tracks pizza orders to DOD; the Abrams variant tracks late-night commits. Spikes precede shipped features.',
    interpretation: value > 15 ? 'SHIPPING IMMINENT' : value > 5 ? 'NIGHT BUILD ACTIVE' : 'CALM',
  };
}

// ── index 5 — Waffle House Stack Index (green/yellow/red) ────────────
function waffleHouseIndex() {
  const pm2 = pm2Status();
  const ratio = pm2.all ? pm2.online / pm2.all : 1;
  let value, color, interpretation;
  if (ratio === 1) { value = 'GREEN'; color = '#4ade80'; interpretation = 'FULL MENU — all services online'; }
  else if (ratio >= 0.8) { value = 'YELLOW'; color = '#f59e0b'; interpretation = 'LIMITED MENU — degraded operations'; }
  else if (ratio >= 0.5) { value = 'ORANGE'; color = '#fb923c'; interpretation = 'GRILL OFF — major outage'; }
  else { value = 'RED'; color = '#ef4444'; interpretation = 'CLOSED — critical infrastructure down'; }
  return {
    id: 'WHSI',
    name: 'Waffle House Stack Index',
    icon: '🧇',
    value,
    color,
    unit: pm2.online + '/' + pm2.all,
    delta: pm2.down ? -pm2.down : 0,
    delta_label: pm2.down ? `${pm2.down} down · ${pm2.restarting} restarting` : 'all systems operational',
    sparkline: null,
    method: "FEMA's Waffle House Index measures disaster severity by whether Waffle House is serving. The Abrams variant: GREEN if every pm2 process online, YELLOW if 80-99%, ORANGE if 50-79%, RED if <50%. Real-time stack health.",
    interpretation,
  };
}

// ── index 6 — Lead Velocity Index (leads/week, 7-day rolling) ─────────
function leadVelocityIndex() {
  const leadsFile = path.join(ROOT, 'data', 'leads.jsonl');
  let leads = [];
  try { leads = fs.readFileSync(leadsFile, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)); } catch {}
  const now = Date.now();
  const last7 = leads.filter(l => now - new Date(l.ts).getTime() < 7 * 86400000).length;
  const prev7 = leads.filter(l => {
    const d = now - new Date(l.ts).getTime();
    return d < 14 * 86400000 && d >= 7 * 86400000;
  }).length;
  // 7-day daily sparkline
  const daily = {};
  for (const l of leads) {
    const day = (l.ts || '').slice(0, 10);
    if (day) daily[day] = (daily[day] || 0) + 1;
  }
  const series = [];
  for (let i = 6; i >= 0; i--) {
    const day = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
    series.push(daily[day] || 0);
  }
  return {
    id: 'LVI',
    name: 'Lead Velocity Index',
    icon: '⟶',
    value: last7,
    unit: 'leads / 7d',
    delta: last7 - prev7,
    delta_label: 'vs prior 7d',
    sparkline: series,
    method: 'Leads captured in the last 7 days via /api/lead. Compare to prior 7d for momentum. Baseline build phase = 0-1; growth phase = 5+.',
    interpretation: last7 >= 5 ? 'INBOUND PIPELINE' : last7 >= 1 ? 'EARLY SIGNAL' : 'PRE-LAUNCH',
  };
}

// ── index 7 — Trademark Confidence Index (qualitative score) ──────────
function tmConfidenceIndex() {
  const tm = readJson(path.join(ROOT, 'data', 'trademark.json'), null);
  if (!tm) return null;
  let score = 0;
  const checks = [
    { rule: 'Mark identified',          ok: !!tm.mark,                              pts: 10 },
    { rule: 'Owner identified',         ok: !!tm.owner,                             pts: 10 },
    { rule: '5 classes selected',       ok: (tm.recommended_classes || []).length >= 5, pts: 20 },
    { rule: 'Filing routes researched', ok: (tm.filing_routes || []).length >= 3,   pts: 15 },
    { rule: 'Domain status assessed',   ok: !!tm.domain_status,                     pts: 10 },
    { rule: 'Common-law rights noted',  ok: !!tm.common_law_status,                 pts: 10 },
    { rule: 'Next-action plan in place', ok: (tm.next_actions || []).length >= 3,   pts: 10 },
    { rule: 'Filing submitted to USPTO', ok: false,                                 pts: 15 }, // TODO when filed
  ];
  for (const c of checks) if (c.ok) score += c.pts;
  return {
    id: 'TMCI',
    name: 'TM Confidence Index',
    icon: '®',
    value: score,
    unit: '/100',
    delta: 0,
    delta_label: `${checks.filter(c => c.ok).length}/${checks.length} steps complete`,
    sparkline: null,
    method: 'Weighted checklist: mark+owner identified (20pt), 5+ classes selected (20), filing routes researched (15), domain status (10), common-law noted (10), next-action plan (10), USPTO filing submitted (15).',
    interpretation: score >= 85 ? 'FILED' : score >= 60 ? 'READY TO FILE' : score >= 30 ? 'IN PREP' : 'EARLY',
  };
}

// ── index 8 — Empire Sprawl Index (total LOC across portfolio) ────────
function empireSprawlIndex() {
  const loc = totalLoc();
  return {
    id: 'ESI',
    name: 'Empire Sprawl Index',
    icon: '✦',
    value: loc,
    unit: 'LOC',
    delta: null,
    delta_label: `across ${abramsDirs().length} projects`,
    sparkline: null,
    method: 'Total lines of code across every Abrams project (.js / .ts / .tsx / .html / .css / .py / .sh / .md, excluding node_modules + .git). One scalar for the size of the empire.',
    interpretation: loc > 100000 ? 'SOLO-FOUNDER MEGAPROJECT' : loc > 25000 ? 'MID-EMPIRE' : 'EARLY',
  };
}

// ── run ───────────────────────────────────────────────────────────────
const indexes = [
  buildIndex(),
  operatorHeatIndex(),
  lateNightIndex(),
  pizzaIndex(),
  waffleHouseIndex(),
  leadVelocityIndex(),
  tmConfidenceIndex(),
  empireSprawlIndex(),
].filter(Boolean);

const out = { generated_at: new Date().toISOString(), indexes };
fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
log('abrams-indexes', { count: indexes.length });
console.log(`abrams-indexes: ${indexes.length} indexes → ${OUT}`);
for (const ix of indexes) console.log(`  ${ix.id.padEnd(6)} ${String(ix.value).padEnd(8)} ${ix.unit.padEnd(20)} ${ix.interpretation}`);