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