@rosh100yx/outlier 0.4.25 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -5,6 +5,9 @@ import pc from 'picocolors';
5
5
  import { getAuthorshipStats } from './git';
6
6
  import { getCarbonStats } from './carbon';
7
7
  import { getCapabilitiesStats } from './capabilities';
8
+ import { deriveInsights, type Insight } from './insights';
9
+ import { projectEconomics } from './economics';
10
+ import { aggregateDir } from './aggregate';
8
11
  import { writeFileSync, readFileSync, chmodSync, existsSync } from 'fs';
9
12
  import { join } from 'path';
10
13
  import { detectAgent } from './agent';
@@ -20,6 +23,110 @@ const ASCII_LOGO = `
20
23
 
21
24
  let finalReceipt = '';
22
25
 
26
+ // Turn the left-rail receipt into a clean closed rectangle (adds the right border,
27
+ // padding each line to a fixed inner width). Width-aware: strips ANSI and counts a few
28
+ // known wide glyphs as 2 columns so the right edge lines up in a terminal and on GitHub.
29
+ function closeBox(s: string, W = 66): string {
30
+ const wide = new Set(['⚠', '🛑', '✈', '🌱', '📸', '🔬', '💾', '💡', '✅', '❌', '😾', '😀']);
31
+ const chW = (ch: string) => { const cp = ch.codePointAt(0)!; return (cp >= 0x1F000 || wide.has(ch)) ? 2 : 1; };
32
+ const rail = '\x1b[2m│\x1b[0m';
33
+ // Fit a (possibly ANSI-coloured) line to exactly `totalVis` visible columns: pad with
34
+ // spaces, or truncate with an ellipsis — preserving colour codes either way.
35
+ const fit = (line: string, totalVis: number) => {
36
+ const parts = line.split(/(\x1b\[[0-9;]*m)/);
37
+ let out = '', vis = 0, cut = false;
38
+ for (const p of parts) {
39
+ if (/^\x1b\[/.test(p)) { out += p; continue; }
40
+ for (const ch of p) {
41
+ const w = chW(ch);
42
+ if (vis + w > totalVis - 1) { cut = true; break; }
43
+ out += ch; vis += w;
44
+ }
45
+ if (cut) break;
46
+ }
47
+ if (cut) { out += '…'; vis += 1; }
48
+ return out + ' '.repeat(Math.max(0, totalVis - vis)) + '\x1b[0m';
49
+ };
50
+ return s.split('\n').map(line => {
51
+ const plain = line.replace(/\x1b\[[0-9;]*m/g, '');
52
+ if (/^\s*┌/.test(plain)) return ' \x1b[2m┌' + '─'.repeat(W) + '┐\x1b[0m';
53
+ if (/^\s*├/.test(plain)) return ' \x1b[2m├' + '─'.repeat(W) + '┤\x1b[0m';
54
+ if (/^\s*└/.test(plain)) return ' \x1b[2m└' + '─'.repeat(W) + '┘\x1b[0m';
55
+ if (/^\s*│/.test(plain)) return fit(line, 2 + W) + rail; // 2 = leading space + left rail
56
+ return line;
57
+ }).join('\n');
58
+ }
59
+
60
+ // Build a stable, machine-readable audit object. This is the contract agents,
61
+ // swarms, and CI parse — everything the human receipt shows, as plain JSON.
62
+ async function emitJson() {
63
+ const pkg = require('../package.json');
64
+ const [gitStats, carbon, caps] = await Promise.all([
65
+ getAuthorshipStats().catch(() => null),
66
+ getCarbonStats().catch(() => null),
67
+ getCapabilitiesStats().catch(() => null),
68
+ ]);
69
+
70
+ const aiRatio = gitStats ? gitStats.ratio : 0;
71
+ const cap = 0.70;
72
+ const writeOrDeploy = caps
73
+ ? caps.mcps.filter((m: any) => ['money', 'exec', 'deploy', 'write-remote', 'write-local'].includes(m.reach)).length
74
+ : 0;
75
+
76
+ const out = {
77
+ tool: 'outlier',
78
+ version: pkg.version,
79
+ repo: process.cwd().split('/').pop(),
80
+ generatedAt: new Date().toISOString(),
81
+ localFirst: true,
82
+ authorship: gitStats ? {
83
+ aiPercent: +(gitStats.ratio * 100).toFixed(1),
84
+ aiRatio: gitStats.ratio,
85
+ totalCommits: gitStats.total,
86
+ aiCommits: gitStats.ai,
87
+ nonMergePercent: +(gitStats.ratioNoMerges * 100).toFixed(1),
88
+ provenance: 'proxy',
89
+ note: 'git Co-Authored-By trailers; under-counts if the agent omits the trailer',
90
+ } : null,
91
+ cost: carbon ? {
92
+ totalTokens: carbon.totalTokens,
93
+ outputTokens: carbon.outputTokens,
94
+ cacheReusePercent: carbon.totalTokens ? +((carbon.cacheReadTokens / carbon.totalTokens) * 100).toFixed(1) : 0,
95
+ estUsd: +carbon.estUsd.toFixed(2),
96
+ costIsReal: carbon.costIsReal,
97
+ provenance: carbon.tokenProvenance,
98
+ source: carbon.sourceLabel,
99
+ } : null,
100
+ carbon: carbon ? {
101
+ energyKwh: +carbon.energyKwh.toFixed(4),
102
+ co2Kg: +carbon.localCo2Kg.toFixed(4),
103
+ region: carbon.localRegion,
104
+ provenance: carbon.carbonProvenance,
105
+ note: 'counterfactual: cloud inference runs on the provider grid, not yours',
106
+ } : null,
107
+ reach: caps ? {
108
+ blastRadius: caps.blastRadius,
109
+ reasons: caps.blastReasons,
110
+ toolCount: caps.mcps.length,
111
+ writeOrDeployCount: writeOrDeploy,
112
+ tools: caps.mcps,
113
+ subagents: caps.subagents,
114
+ hooks: caps.hooks,
115
+ skills: caps.skills.length,
116
+ orchestration: caps.hasOrchestration,
117
+ } : null,
118
+ policy: {
119
+ aiCapPercent: cap * 100,
120
+ status: aiRatio > cap ? 'over' : 'within',
121
+ },
122
+ insights: deriveInsights({ authorship: gitStats, carbon, caps, policyCap: cap }),
123
+ economics: projectEconomics({ aiRatio, estUsdSession: carbon ? carbon.estUsd : 0, teamSize: 1 }),
124
+ };
125
+
126
+ // Only JSON on stdout — nothing else.
127
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
128
+ }
129
+
23
130
  async function runOnboarding() {
24
131
  console.log(pc.cyan(ASCII_LOGO));
25
132
  intro(pc.inverse(' outlier: Welcome '));
@@ -75,22 +182,39 @@ async function main() {
75
182
  } catch (e) {}
76
183
  }
77
184
  if (alreadyRun) process.exit(0);
78
- action = 'status';
185
+ // Compact once-per-day greeting (for the Claude Code plugin SessionStart hook) —
186
+ // a single line, fast, no full receipt.
187
+ const g = await getAuthorshipStats().catch(() => null);
188
+ const cp = await getCapabilitiesStats().catch(() => null);
189
+ const aiP = g ? (g.ratio * 100).toFixed(0) + '%' : '—';
190
+ const br = cp ? cp.blastRadius : '—';
191
+ const brc = br === 'HIGH' || br === 'CRITICAL' ? pc.red : br === 'MEDIUM' ? pc.yellow : pc.green;
192
+ console.log(`${pc.dim('[outlier]')} AI authorship ${pc.bold(aiP)} · agent reach ${brc(pc.bold(br))} ${pc.dim('· before you delegate, run: outlier preflight')}`);
193
+ process.exit(0);
194
+ }
195
+
196
+ // Agent / CI / swarm contract: --json emits a structured audit and nothing else
197
+ // (no logo, no spinner, no ANSI). This is how an agent perceives outlier.
198
+ if (process.argv.includes('--json')) {
199
+ await emitJson();
200
+ process.exit(0);
79
201
  }
80
202
 
81
203
  console.log(pc.cyan(ASCII_LOGO));
82
204
  const pkg = require('../package.json');
83
205
  console.log(pc.dim(` Outlier v${pkg.version} · AI Code Reliance & Telemetry Engine\n`));
84
-
85
-
206
+
207
+
86
208
  if (action === '--help' || action === '-h' || action === 'help') {
87
209
  console.log(pc.bold('\nWHAT OUTLIER DOES'));
88
210
  console.log(pc.dim(' Reads your local git history and AI logs — on your machine — to show'));
89
211
  console.log(pc.dim(' how much of your code AI wrote, what it cost, and how to keep your skill.\n'));
90
212
  console.log(pc.bold('COMMANDS:'));
91
213
  console.log(` ${pc.cyan('outlier')} Run the audit (the default — same as 'status')`);
214
+ console.log(` ${pc.cyan('outlier preflight')} Quick briefing BEFORE you start an agent (reach + skill + spend)`);
92
215
  console.log(` ${pc.cyan('outlier status')} Full audit: who wrote the code, what it cost, your limit`);
93
216
  console.log(` ${pc.cyan('outlier status --save')} Save the audit to ./outlier-audit.txt`);
217
+ console.log(` ${pc.cyan('outlier --json')} Machine-readable audit (for agents, CI, swarms)`);
94
218
  console.log(` ${pc.cyan('outlier authorship')} Just the AI-vs-human commit breakdown`);
95
219
  console.log(` ${pc.cyan('outlier carbon')} Just the token spend, cache waste & carbon`);
96
220
  console.log(` ${pc.cyan('outlier capabilities')} What tools & skills your agents can reach`);
@@ -301,14 +425,37 @@ Conservative Floor: ${color(nmPct + '%')}`,
301
425
  let cachePct = '0';
302
426
  let co2Str = '0.0kg';
303
427
  let regionStr = 'Global Average';
428
+ let sourceLabel = 'no local AI logs found';
429
+ let noData = true;
304
430
  if (carbon) {
305
431
  if (carbon.totalTokens > 0) {
306
432
  cachePct = ((carbon.cacheReadTokens / carbon.totalTokens) * 100).toFixed(1);
433
+ noData = false;
307
434
  }
308
435
  co2Str = `${carbon.localCo2Kg.toFixed(2)}kg CO2`;
309
436
  regionStr = carbon.localRegion;
437
+ sourceLabel = carbon.sourceLabel;
438
+ }
439
+
440
+ // One-line agent-reach summary (full detail in `outlier capabilities`).
441
+ let reachStr = pc.dim('run: outlier capabilities');
442
+ if (capabilities) {
443
+ const rc = capabilities.blastRadius;
444
+ const col = rc === 'CRITICAL' || rc === 'HIGH' ? pc.red : rc === 'MEDIUM' ? pc.yellow : pc.green;
445
+ const risky = capabilities.mcps.filter((m: any) => ['money','exec','deploy','write-remote','write-local'].includes(m.reach)).length;
446
+ reachStr = `${col(pc.bold(rc))} · ${capabilities.mcps.length} tools` + (risky ? pc.dim(`, ${risky} can write/deploy`) : '');
310
447
  }
311
448
 
449
+ // Insight engine: turn the numbers into the top thing to actually do.
450
+ const insights = deriveInsights({ authorship: gitStats, carbon, caps: capabilities, policyCap: 0.70 });
451
+ const sevColor = (s: string) => s === 'critical' ? pc.red : s === 'warn' ? pc.yellow : s === 'good' ? pc.green : pc.cyan;
452
+ const sevMark = (s: string) => s === 'critical' ? '✗' : s === 'warn' ? '⚠' : s === 'good' ? '✓' : 'i';
453
+ const insightLines = insights.slice(0, 2).map((ins: Insight) =>
454
+ ` ${pc.dim('│')} ${sevColor(ins.severity)(sevMark(ins.severity))} ${pc.bold(ins.title)}\n` +
455
+ ` ${pc.dim('│')} ${ins.detail.length > 56 ? ins.detail.slice(0, 55) + '…' : ins.detail}\n` +
456
+ ` ${pc.dim('│')} ${pc.cyan('→ ' + ins.action)}`
457
+ ).join(`\n ${pc.dim('│')}\n`);
458
+
312
459
  // The thermal receipt below is the single canonical output for `status`.
313
460
  // (The old @clack dashboard panel was removed: it duplicated the receipt's
314
461
  // numbers in a second format, doubling the output on every run.)
@@ -370,13 +517,21 @@ Conservative Floor: ${color(nmPct + '%')}`,
370
517
  ${pc.dim('│')} Tokens used ${pc.bold(totalTokensStr)}
371
518
  ${pc.dim('│')} Est. spend ${pc.bold(estUsdStr)}
372
519
  ${pc.dim('│')} Re-used context ${cacheBar} ${pc.bold(cachePct + '%')}
373
- ${pc.dim('│')} Energy ${pc.bold(co2Str)} ${pc.dim(`(${regionStr} grid, rough)`)}
520
+ ${pc.dim('│')} Energy ${pc.bold(co2Str)} ${pc.dim(`(${regionStr} grid)`)}
521
+ ${pc.dim('│')} ${pc.dim(`Source: ${sourceLabel}`)}
374
522
  ${pc.dim('│')}
375
523
  ${pc.dim('│')} ${cacheVerdict} — ${cacheText.split('\n').join('\n ' + pc.dim('│') + ' ')}
376
524
  ${pc.dim('├────────────────────────────────────────────────────────')}
525
+ ${pc.dim('│')} ${pc.bold(pc.bgCyan(pc.black(' WHAT YOUR AGENTS CAN REACH ')))}
526
+ ${pc.dim('│')} Blast radius ${reachStr}
527
+ ${pc.dim('│')} ${pc.dim('Full map (deploy/push/write tools): outlier capabilities')}
528
+ ${pc.dim('├────────────────────────────────────────────────────────')}
377
529
  ${pc.dim('│')} ${pc.bold(pc.bgYellow(pc.black(' YOUR LIMIT ')))}
378
530
  ${pc.dim('│')} AI cap ${pc.bold('70%')} ${pc.dim('· change with: outlier policy')}
379
531
  ${pc.dim('│')} Status ${policyStatus} ${pc.dim('·')} ${policyAction}
532
+ ${pc.dim('├────────────────────────────────────────────────────────')}
533
+ ${pc.dim('│')} ${pc.bold(pc.bgGreen(pc.black(' WHAT TO DO ')))}
534
+ ${insightLines}
380
535
  ${pc.dim('├────────────────────────────────────────────────────────')}
381
536
  ${pc.dim('│')} ${pc.dim('Numbers are local estimates — authorship is a proxy and')}
382
537
  ${pc.dim('│')} ${pc.dim('carbon is rough. How it works: outlier --help')}
@@ -394,29 +549,103 @@ Conservative Floor: ${color(nmPct + '%')}`,
394
549
  }
395
550
 
396
551
  } else if (action === 'capabilities') {
397
- s.start('Auditing AI surface area (MCPs, Skills, Orchestrators)...');
552
+ s.start('Mapping what your agents can reach...');
398
553
  try {
399
554
  const caps = await getCapabilitiesStats();
400
- s.stop('Capabilities Scan Complete');
555
+ s.stop('Reach map complete');
556
+
557
+ const radiusColor = caps.blastRadius === 'CRITICAL' ? pc.red
558
+ : caps.blastRadius === 'HIGH' ? pc.red
559
+ : caps.blastRadius === 'MEDIUM' ? pc.yellow : pc.green;
560
+
561
+ // Group tools by reach so the risky ones stand out.
562
+ const order: string[] = ['money', 'exec', 'deploy', 'write-remote', 'write-local', 'data', 'network', 'model', 'read'];
563
+ const reachLabel: Record<string, string> = {
564
+ money: 'can move money', exec: 'can run shell', deploy: 'can deploy', 'write-remote': 'can push to repos',
565
+ 'write-local': 'can write files', data: 'data stores', network: 'network', model: 'models', read: 'read-only',
566
+ };
567
+ const riskyReaches = new Set(['money', 'exec', 'deploy', 'write-remote', 'write-local']);
568
+ const toolLines = caps.mcps.length === 0 ? ' None detected'
569
+ : order.filter(r => caps.mcps.some(m => m.reach === r)).map(r => {
570
+ const names = caps.mcps.filter(m => m.reach === r).map(m => m.name).join(', ');
571
+ const tag = riskyReaches.has(r) ? pc.red(`[${reachLabel[r]}]`) : pc.dim(`[${reachLabel[r]}]`);
572
+ return ` ${tag} ${names}`;
573
+ }).join('\n');
401
574
 
402
575
  note(
403
- `Orchestration Policy: ${caps.hasOrchestration ? pc.green('Detected (AGENTS.md)') : pc.yellow('None')}
576
+ `${pc.bold('BLAST RADIUS:')} ${radiusColor(pc.bold(caps.blastRadius))} ${pc.dim('— if an agent or a prompt injection drives your tools')}
577
+ ${caps.blastReasons.length ? caps.blastReasons.map(r => ` ${pc.red('•')} ${r}`).join('\n') : pc.green(' • read-only — limited reach')}
404
578
 
405
- Active Skills (${caps.skills.length}):
406
- ${caps.skills.length > 0 ? pc.cyan(caps.skills.map(s => ` • ${s}`).join('\n')) : ' None'}
579
+ ${pc.bold(`What your agents can reach (${caps.mcps.length} MCP tools):`)}
580
+ ${toolLines}
407
581
 
408
- Active MCP Servers (${caps.mcps.length}):
409
- ${caps.mcps.length > 0 ? pc.magenta(caps.mcps.map(m => ` • ${m}`).join('\n')) : ' None'}
582
+ ${pc.bold('Automation & agents:')}
583
+ Hooks that fire for you: ${caps.hooks.length ? pc.yellow(caps.hooks.join(', ')) : 'none'}
584
+ Sub-agents: ${caps.subagents} Skills: ${caps.skills.length} Orchestration policy: ${caps.hasOrchestration ? pc.green('yes') : pc.yellow('no')}
410
585
 
411
- ${pc.bold('Governance Assessment:')}
412
- This repository provides agents with ${caps.mcps.length} toolsets and ${caps.skills.length} skills.
413
- ${caps.skills.length > 5 ? pc.red('⚠ High Surface Area: Ensure strict authorship review is enabled.') : pc.green('✓ Low Surface Area: Risk contained.')}`,
414
- 'AI Capabilities Map'
586
+ ${pc.dim('This is your attack surface. Fewer write/deploy tools per session = smaller blast radius.')}`,
587
+ 'Agent Reach & Blast Radius'
415
588
  );
416
589
  } catch (e: any) {
417
590
  s.stop('Audit failed');
418
591
  console.error(pc.red(e.message));
419
592
  }
593
+ } else if (action === 'aggregate') {
594
+ // Team/fleet rollup from a folder of `outlier --json` files. Local-first, no export.
595
+ const dir = process.argv[3];
596
+ if (!dir || !existsSync(dir)) {
597
+ console.error(pc.red('Usage: outlier aggregate <folder-of-json-audits>'));
598
+ console.log(pc.dim(' Each dev: outlier --json > team/<name>.json then: outlier aggregate team/'));
599
+ process.exit(1);
600
+ }
601
+ const r = aggregateDir(dir);
602
+ note(
603
+ `Developers: ${pc.bold(String(r.developers))}
604
+ Avg AI authorship: ${pc.bold(r.avgAiPercent !== null ? r.avgAiPercent + '%' : '—')} Max: ${r.maxAiPercent !== null ? r.maxAiPercent + '%' : '—'}
605
+ Over their limit: ${r.overLimit > 0 ? pc.red(String(r.overLimit)) : pc.green('0')}
606
+ Team spend (est): ${pc.bold('$' + r.totalEstUsd)}
607
+ Worst blast radius:${' '}${r.worstBlastRadius === 'HIGH' || r.worstBlastRadius === 'CRITICAL' ? pc.red(r.worstBlastRadius) : pc.yellow(r.worstBlastRadius)} (${r.reachWriteDeploy} write/deploy tools across the team)
608
+ ${r.notes.length ? '\n' + r.notes.map(n => `${pc.yellow('•')} ${n}`).join('\n') : ''}`,
609
+ 'Team Rollup (local-first — nothing was exported)'
610
+ );
611
+ } else if (action === 'preflight') {
612
+ // Forward-looking briefing — the reason to run outlier BEFORE you start an agent.
613
+ // Same engine as status, framed for the session you are about to begin: reach,
614
+ // skill, spend, and the one thing to do, ending with the handoff to your agent.
615
+ s.start('Pre-flight check...');
616
+ const gitStats = await getAuthorshipStats().catch(() => null);
617
+ const carbon = await getCarbonStats().catch(() => null);
618
+ const caps = await getCapabilitiesStats().catch(() => null);
619
+ s.stop('Ready for take-off');
620
+
621
+ const aiPct = gitStats ? (gitStats.ratio * 100).toFixed(0) : '—';
622
+ const youPct = gitStats ? (100 - gitStats.ratio * 100).toFixed(0) : '—';
623
+ const blast = caps ? caps.blastRadius : 'UNKNOWN';
624
+ const blastCol = blast === 'CRITICAL' || blast === 'HIGH' ? pc.red : blast === 'MEDIUM' ? pc.yellow : pc.green;
625
+ const risky = caps ? caps.mcps.filter(m => ['money','exec','deploy','write-remote','write-local'].includes(m.reach)).length : 0;
626
+ const spend = carbon ? `$${carbon.estUsd.toFixed(0)}` : '—';
627
+ const cachePct = carbon && carbon.totalTokens ? ((carbon.cacheReadTokens / carbon.totalTokens) * 100).toFixed(0) + '%' : '—';
628
+
629
+ const insights = deriveInsights({ authorship: gitStats, carbon, caps, policyCap: 0.70 });
630
+ const sevCol = (sv: string) => sv === 'critical' ? pc.red : sv === 'warn' ? pc.yellow : sv === 'good' ? pc.green : pc.cyan;
631
+ const sevMk = (sv: string) => sv === 'critical' ? '✗' : sv === 'warn' ? '⚠' : sv === 'good' ? '✓' : 'i';
632
+ const actionLines = insights.slice(0, 3)
633
+ .map(ins => ` ${sevCol(ins.severity)(sevMk(ins.severity))} ${pc.cyan('→')} ${ins.action}`)
634
+ .join('\n');
635
+
636
+ console.log('');
637
+ console.log(pc.bold(pc.cyan(' ✈ PRE-FLIGHT')) + pc.dim(` · ${process.cwd().split('/').pop()}`));
638
+ console.log(pc.dim(' ────────────────────────────────────────────────────'));
639
+ console.log(` ${pc.bold('Reach')} ${blastCol(pc.bold(blast))}` + (caps ? pc.dim(` · ${caps.mcps.length} tools, ${risky} can write/deploy`) : ''));
640
+ console.log(` ${pc.bold('Skill')} AI wrote ${pc.bold(aiPct + '%')} · you own ${pc.bold(youPct + '%')}`);
641
+ console.log(` ${pc.bold('Spend')} ${pc.bold(spend)} · ${cachePct} re-sent context`);
642
+ console.log('');
643
+ console.log(pc.bold(' Before you delegate:'));
644
+ console.log(actionLines);
645
+ console.log('');
646
+ const agent = detectAgent();
647
+ console.log(pc.bold(pc.magenta(' ✓ Ready? ')) + 'Start your session: ' + pc.bold(agent || 'your AI agent'));
648
+ console.log('');
420
649
  } else if (action === 'policy') {
421
650
  const tier = await select({
422
651
  message: 'Select the governance tier to configure:',
@@ -501,19 +730,34 @@ Enforcement: ${pc.cyan('Local pre-commit hook installed (backup created)')}`,
501
730
  'Active Governance Policy'
502
731
  );
503
732
  } else if (tier === 'regulatory') {
504
- s.start('Generating Regulatory Compliance Audit (Decree 142)...');
505
- await new Promise(resolve => setTimeout(resolve, 1200));
506
-
733
+ s.start('Generating human-oversight audit record (Decree 142)...');
734
+ // A real, honest compliance record from the actual local numbers — not a stub.
735
+ const gitStats = await getAuthorshipStats().catch(() => null);
736
+ const caps = await getCapabilitiesStats().catch(() => null);
737
+ const humanReviewRate = gitStats ? +(1 - gitStats.ratio).toFixed(3) : null;
738
+ const oversightOk = humanReviewRate !== null && humanReviewRate >= 0.30; // ≥30% human-authored
739
+ const record = {
740
+ timestamp: new Date().toISOString(),
741
+ policy: 'Vietnam Decree 142/2026 — human oversight of high-risk AI',
742
+ repo: process.cwd().split('/').pop(),
743
+ humanAuthorshipRate: humanReviewRate,
744
+ aiAuthorshipRate: gitStats ? +gitStats.ratio.toFixed(3) : null,
745
+ humanOversight: oversightOk ? 'present' : 'insufficient',
746
+ agentBlastRadius: caps ? caps.blastRadius : 'unknown',
747
+ dataExported: false,
748
+ note: 'Derived from local git history only. No code, prompts, or citizen data leave the machine. Authorship is a proxy for human oversight.',
749
+ };
507
750
  const reportPath = join(process.cwd(), 'outlier-audit-report.jsonl');
508
- writeFileSync(reportPath, JSON.stringify({ timestamp: new Date().toISOString(), status: 'PREVIEW', policy: 'Decree 142', simulatedOversight: true }) + '\n');
509
- s.stop('Audit Generated');
751
+ writeFileSync(reportPath, JSON.stringify(record) + '\n');
752
+ s.stop('Audit record written');
510
753
 
511
754
  note(
512
- `Jurisdiction: ${pc.bold('Vietnam (Decree 142)')}
513
- Status: ${pc.green('Compliant - Human oversight logged locally')}
514
- Privacy: ${pc.green('Preserved - No citizen data exported')}
755
+ `Jurisdiction: ${pc.bold('Vietnam (Decree 142/2026)')}
756
+ Human oversight: ${oversightOk ? pc.green('present') : pc.red('insufficient')} ${pc.dim(`(${humanReviewRate !== null ? (humanReviewRate * 100).toFixed(0) + '% human-authored' : 'no git history'})`)}
757
+ Agent reach: ${caps ? caps.blastRadius : 'unknown'}
758
+ Privacy: ${pc.green('preserved — nothing exported')}
515
759
  Artifact: ${pc.cyan(reportPath)}`,
516
- 'Regulatory Compliance'
760
+ 'Human-Oversight Audit Record'
517
761
  );
518
762
  }
519
763
  } else if (action === 'participate') {
@@ -576,6 +820,25 @@ Artifact: ${pc.cyan(reportPath)}`,
576
820
  console.log(` ${pc.red('Lose:')} Architectural intimacy. You become a reviewer.`);
577
821
  console.log(pc.cyan('\n■ Next 5-10 Years (The 1M+ LOC Crisis)'));
578
822
  console.log(` When an agent introduces a fatal state bug in a monolithic architecture, human reviewers will lack the muscle memory to debug it. Outlier measures this exact sovereignty erosion.\n`);
823
+
824
+ // Economic translation: the macro shadow of your individual number.
825
+ const gitStats = await getAuthorshipStats().catch(() => null);
826
+ const carbon = await getCarbonStats().catch(() => null);
827
+ if (gitStats || carbon) {
828
+ const { projectEconomics } = await import('./economics');
829
+ const teamSize = (() => { const i = process.argv.indexOf('--team'); return i > -1 ? (parseInt(process.argv[i + 1] || '1') || 1) : 1; })();
830
+ const econ = projectEconomics({
831
+ aiRatio: gitStats ? gitStats.ratio : 0,
832
+ estUsdSession: carbon ? carbon.estUsd : 0,
833
+ teamSize,
834
+ });
835
+ console.log(pc.bold(pc.bgMagenta(' THE MACRO SHADOW ')) + pc.dim(` (team of ${teamSize} — set with --team N)`));
836
+ for (const p of econ.projections) {
837
+ console.log(` ${pc.bold(p.label.padEnd(20))} ${pc.cyan(p.value)}`);
838
+ console.log(` ${pc.dim(' ' + p.note)}`);
839
+ }
840
+ console.log('\n' + pc.dim(' ' + econ.assumptions) + '\n');
841
+ }
579
842
  } else if (action === 'knowledge') {
580
843
  console.log('\n' + pc.bold(pc.bgBlue(' CORE LITERATURE & REFERENCES ')) + '\n');
581
844
  console.log(`1. ${pc.cyan('METR (Measuring AI Ability)')} - Evaluating AI on long-horizon software tasks.`);
@@ -587,14 +850,15 @@ Artifact: ${pc.cyan(reportPath)}`,
587
850
  outro('Done — nothing left your machine. (How it works: outlier --help)');
588
851
 
589
852
  if (typeof finalReceipt !== 'undefined' && finalReceipt) {
590
- console.log(finalReceipt);
853
+ const boxed = closeBox(finalReceipt);
854
+ console.log(boxed);
591
855
 
592
856
  // --save: write a plain-text (no color) copy of the receipt next to the repo.
593
857
  if (process.argv.includes('--save')) {
594
858
  const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, '');
595
859
  const savePath = join(process.cwd(), 'outlier-audit.txt');
596
860
  try {
597
- writeFileSync(savePath, stripAnsi(finalReceipt).trimStart() + '\n');
861
+ writeFileSync(savePath, stripAnsi(boxed).trimStart() + '\n');
598
862
  console.log(pc.dim(`\n 💾 Saved to ${savePath}`));
599
863
  } catch {}
600
864
  }
@@ -0,0 +1,66 @@
1
+ // Economic translation layer.
2
+ //
3
+ // The paper's core claim is that an individual authorship/spend number has a macro
4
+ // shadow: at team and national scale it becomes skill-ladder erosion, offshore value
5
+ // capture, and a hard-currency (forex) drain. This module turns the local numbers into
6
+ // that projection — explicitly as a *projection with stated assumptions*, not a measurement.
7
+
8
+ export interface EconProjection {
9
+ label: string;
10
+ value: string;
11
+ note: string;
12
+ }
13
+
14
+ export interface EconInput {
15
+ aiRatio: number; // 0..1, share of authored output that is AI
16
+ estUsdSession: number; // measured/estimated spend reflected by local logs
17
+ teamSize?: number; // default 1 (solo); orgs override
18
+ workdaysPerYear?: number;
19
+ }
20
+
21
+ // Assumptions are surfaced in the output so nothing is hidden.
22
+ const DEFAULTS = { teamSize: 1, workdaysPerYear: 230 };
23
+
24
+ export function projectEconomics(input: EconInput): { projections: EconProjection[]; assumptions: string } {
25
+ const team = input.teamSize ?? DEFAULTS.teamSize;
26
+ const days = input.workdaysPerYear ?? DEFAULTS.workdaysPerYear;
27
+ const ai = Math.max(0, Math.min(1, input.aiRatio));
28
+
29
+ // Treat the local spend as a per-active-period hard-currency outflow to the AI vendor.
30
+ // Annualize crudely (the local logs are a sample window, not a day) and scale by team.
31
+ const annualOutflowPerDev = input.estUsdSession * (days / 30); // sample window ≈ a month of work
32
+ const annualOutflowTeam = annualOutflowPerDev * team;
33
+
34
+ const projections: EconProjection[] = [
35
+ {
36
+ label: 'Authorship shift',
37
+ value: `${(ai * 100).toFixed(0)}% of output → AI`,
38
+ note: team > 1
39
+ ? `≈ ${(ai * team).toFixed(1)} of ${team} dev-equivalents of authorship now machine-produced.`
40
+ : 'At team scale, this many dev-equivalents move to the machine.',
41
+ },
42
+ {
43
+ label: 'Value capture (offshore)',
44
+ value: `~$${Math.round(annualOutflowTeam).toLocaleString()}/yr`,
45
+ note: 'Hard-currency spend leaving to a foreign AI vendor — value captured offshore, not by the local worker.',
46
+ },
47
+ {
48
+ label: 'Skill ladder',
49
+ value: ai > 0.7 ? 'AT RISK' : ai > 0.4 ? 'watch' : 'intact',
50
+ note: ai > 0.7
51
+ ? 'Above ~70%, juniors stop building the skill that makes seniors — premature deprofessionalization.'
52
+ : 'Humans still author enough core work to keep building expertise.',
53
+ },
54
+ {
55
+ label: 'Forex / tax base',
56
+ value: `$${Math.round(annualOutflowPerDev).toLocaleString()}/dev/yr imported`,
57
+ note: 'Locally-taxed wages give way to foreign-billed inference: income-tax erosion + a recurring forex import.',
58
+ },
59
+ ];
60
+
61
+ const assumptions =
62
+ `Projection only. Assumes: team of ${team}, the local log window ≈ one month of work, ` +
63
+ `${days} workdays/yr. Spend is your measured/estimated local outflow scaled up — an order-of-magnitude shadow, not an audit.`;
64
+
65
+ return { projections, assumptions };
66
+ }
@@ -0,0 +1,73 @@
1
+ // Offline, model-aware emissions engine.
2
+ //
3
+ // Local-first means NO network: no Electricity Maps / WattTime API calls. We bundle
4
+ // the coefficients and look them up. This is the same approach CodeCarbon uses for its
5
+ // offline tracker. Two inputs:
6
+ // 1. per-model energy (kWh per 1M output tokens) — output tokens dominate inference cost
7
+ // 2. grid carbon intensity (gCO2 per kWh) for the assumed region
8
+ //
9
+ // All numbers are estimates with wide uncertainty (inference energy varies ~4-20x in the
10
+ // literature). We expose the method so the UI can label provenance honestly. We never
11
+ // claim precision we don't have.
12
+
13
+ // Energy per 1M OUTPUT tokens, by model class (kWh).
14
+ // Grounded in: the paper's measured ~10 kWh / 15.1M output on Opus-class (~0.66);
15
+ // EcoLogits (genai-impact) per-call energy methodology; and the Hugging Face AI Energy
16
+ // Score (which ranks small/mid/large model inference energy). Larger models = more
17
+ // active parameters per token = more energy. These remain order-of-magnitude class
18
+ // estimates (the literature spread is ~4-20x), labelled "estimate" in the UI — not vendor
19
+ // figures. CodeCarbon, when present, overrides these with measured hardware energy.
20
+ const MODEL_ENERGY_KWH_PER_M_OUTPUT: Record<string, number> = {
21
+ 'opus': 0.66, // large frontier (Claude Opus, GPT-4 class)
22
+ 'sonnet': 0.30, // mid (Claude Sonnet, GPT-4o)
23
+ 'haiku': 0.10, // small/fast (Claude Haiku, GPT-4o-mini)
24
+ 'gpt-4': 0.55,
25
+ 'gpt-4o': 0.30,
26
+ 'gpt-5': 0.45,
27
+ 'gemini': 0.35, // Gemini Pro class
28
+ 'flash': 0.10, // Gemini Flash class
29
+ 'local': 0.50, // self-hosted / unknown open weights
30
+ 'default': 0.45, // unknown model -> conservative mid
31
+ };
32
+
33
+ // Map a raw model id (e.g. "claude-opus-4-8", "gpt-4o-mini", "gemini-2.5-flash") to a class.
34
+ export function modelClass(modelId: string): string {
35
+ const m = (modelId || '').toLowerCase();
36
+ if (m.includes('opus')) return 'opus';
37
+ if (m.includes('sonnet')) return 'sonnet';
38
+ if (m.includes('haiku')) return 'haiku';
39
+ if (m.includes('flash') || m.includes('mini')) return 'haiku';
40
+ if (m.includes('gpt-5')) return 'gpt-5';
41
+ if (m.includes('gpt-4o')) return 'gpt-4o';
42
+ if (m.includes('gpt-4')) return 'gpt-4';
43
+ if (m.includes('gemini')) return 'gemini';
44
+ if (m.includes('llama') || m.includes('qwen') || m.includes('mistral') || m.includes('local')) return 'local';
45
+ return 'default';
46
+ }
47
+
48
+ export function energyKwhForModel(modelId: string, outputTokens: number): number {
49
+ const cls = modelClass(modelId);
50
+ const coeff = MODEL_ENERGY_KWH_PER_M_OUTPUT[cls] ?? 0.45;
51
+ return (outputTokens / 1_000_000) * coeff;
52
+ }
53
+
54
+ // Sum energy across a per-model output-token breakdown. Falls back to 'default' when the
55
+ // model is unknown. Returns total kWh.
56
+ export function energyKwhByModel(outputByModel: Record<string, number>): number {
57
+ let kwh = 0;
58
+ for (const [model, out] of Object.entries(outputByModel)) {
59
+ kwh += energyKwhForModel(model, out);
60
+ }
61
+ return kwh;
62
+ }
63
+
64
+ export interface EmissionsResult {
65
+ energyKwh: number;
66
+ co2Kg: number;
67
+ gridFactor: number;
68
+ method: string; // human-readable provenance for the UI
69
+ }
70
+
71
+ export function co2FromEnergy(energyKwh: number, gridFactorGPerKwh: number): number {
72
+ return (energyKwh * gridFactorGPerKwh) / 1000; // kg
73
+ }