@peakinfer/cli 1.0.133

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.
Files changed (367) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.env.example +6 -0
  3. package/.github/workflows/peakinfer.yml +64 -0
  4. package/CHANGELOG.md +31 -0
  5. package/LICENSE +190 -0
  6. package/README.md +335 -0
  7. package/data/inferencemax.json +274 -0
  8. package/dist/agent-analyzer.d.ts +45 -0
  9. package/dist/agent-analyzer.d.ts.map +1 -0
  10. package/dist/agent-analyzer.js +374 -0
  11. package/dist/agent-analyzer.js.map +1 -0
  12. package/dist/agent.d.ts +76 -0
  13. package/dist/agent.d.ts.map +1 -0
  14. package/dist/agent.js +965 -0
  15. package/dist/agent.js.map +1 -0
  16. package/dist/agents/correlation-analyzer.d.ts +34 -0
  17. package/dist/agents/correlation-analyzer.d.ts.map +1 -0
  18. package/dist/agents/correlation-analyzer.js +261 -0
  19. package/dist/agents/correlation-analyzer.js.map +1 -0
  20. package/dist/agents/index.d.ts +91 -0
  21. package/dist/agents/index.d.ts.map +1 -0
  22. package/dist/agents/index.js +111 -0
  23. package/dist/agents/index.js.map +1 -0
  24. package/dist/agents/runtime-analyzer.d.ts +38 -0
  25. package/dist/agents/runtime-analyzer.d.ts.map +1 -0
  26. package/dist/agents/runtime-analyzer.js +244 -0
  27. package/dist/agents/runtime-analyzer.js.map +1 -0
  28. package/dist/analysis-types.d.ts +500 -0
  29. package/dist/analysis-types.d.ts.map +1 -0
  30. package/dist/analysis-types.js +11 -0
  31. package/dist/analysis-types.js.map +1 -0
  32. package/dist/analytics.d.ts +25 -0
  33. package/dist/analytics.d.ts.map +1 -0
  34. package/dist/analytics.js +94 -0
  35. package/dist/analytics.js.map +1 -0
  36. package/dist/analyzer.d.ts +48 -0
  37. package/dist/analyzer.d.ts.map +1 -0
  38. package/dist/analyzer.js +547 -0
  39. package/dist/analyzer.js.map +1 -0
  40. package/dist/artifacts.d.ts +44 -0
  41. package/dist/artifacts.d.ts.map +1 -0
  42. package/dist/artifacts.js +165 -0
  43. package/dist/artifacts.js.map +1 -0
  44. package/dist/benchmarks/index.d.ts +88 -0
  45. package/dist/benchmarks/index.d.ts.map +1 -0
  46. package/dist/benchmarks/index.js +205 -0
  47. package/dist/benchmarks/index.js.map +1 -0
  48. package/dist/cli.d.ts +3 -0
  49. package/dist/cli.d.ts.map +1 -0
  50. package/dist/cli.js +427 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/commands/ci.d.ts +19 -0
  53. package/dist/commands/ci.d.ts.map +1 -0
  54. package/dist/commands/ci.js +253 -0
  55. package/dist/commands/ci.js.map +1 -0
  56. package/dist/commands/config.d.ts +16 -0
  57. package/dist/commands/config.d.ts.map +1 -0
  58. package/dist/commands/config.js +249 -0
  59. package/dist/commands/config.js.map +1 -0
  60. package/dist/commands/demo.d.ts +15 -0
  61. package/dist/commands/demo.d.ts.map +1 -0
  62. package/dist/commands/demo.js +106 -0
  63. package/dist/commands/demo.js.map +1 -0
  64. package/dist/commands/export.d.ts +14 -0
  65. package/dist/commands/export.d.ts.map +1 -0
  66. package/dist/commands/export.js +209 -0
  67. package/dist/commands/export.js.map +1 -0
  68. package/dist/commands/history.d.ts +15 -0
  69. package/dist/commands/history.d.ts.map +1 -0
  70. package/dist/commands/history.js +389 -0
  71. package/dist/commands/history.js.map +1 -0
  72. package/dist/commands/template.d.ts +14 -0
  73. package/dist/commands/template.d.ts.map +1 -0
  74. package/dist/commands/template.js +341 -0
  75. package/dist/commands/template.js.map +1 -0
  76. package/dist/commands/validate-map.d.ts +12 -0
  77. package/dist/commands/validate-map.d.ts.map +1 -0
  78. package/dist/commands/validate-map.js +274 -0
  79. package/dist/commands/validate-map.js.map +1 -0
  80. package/dist/commands/whatif.d.ts +17 -0
  81. package/dist/commands/whatif.d.ts.map +1 -0
  82. package/dist/commands/whatif.js +206 -0
  83. package/dist/commands/whatif.js.map +1 -0
  84. package/dist/comparison.d.ts +38 -0
  85. package/dist/comparison.d.ts.map +1 -0
  86. package/dist/comparison.js +223 -0
  87. package/dist/comparison.js.map +1 -0
  88. package/dist/config.d.ts +42 -0
  89. package/dist/config.d.ts.map +1 -0
  90. package/dist/config.js +158 -0
  91. package/dist/config.js.map +1 -0
  92. package/dist/connectors/helicone.d.ts +9 -0
  93. package/dist/connectors/helicone.d.ts.map +1 -0
  94. package/dist/connectors/helicone.js +106 -0
  95. package/dist/connectors/helicone.js.map +1 -0
  96. package/dist/connectors/index.d.ts +37 -0
  97. package/dist/connectors/index.d.ts.map +1 -0
  98. package/dist/connectors/index.js +65 -0
  99. package/dist/connectors/index.js.map +1 -0
  100. package/dist/connectors/langsmith.d.ts +9 -0
  101. package/dist/connectors/langsmith.d.ts.map +1 -0
  102. package/dist/connectors/langsmith.js +122 -0
  103. package/dist/connectors/langsmith.js.map +1 -0
  104. package/dist/connectors/types.d.ts +83 -0
  105. package/dist/connectors/types.d.ts.map +1 -0
  106. package/dist/connectors/types.js +98 -0
  107. package/dist/connectors/types.js.map +1 -0
  108. package/dist/cost-estimator.d.ts +46 -0
  109. package/dist/cost-estimator.d.ts.map +1 -0
  110. package/dist/cost-estimator.js +104 -0
  111. package/dist/cost-estimator.js.map +1 -0
  112. package/dist/costs.d.ts +57 -0
  113. package/dist/costs.d.ts.map +1 -0
  114. package/dist/costs.js +251 -0
  115. package/dist/costs.js.map +1 -0
  116. package/dist/counterfactuals.d.ts +29 -0
  117. package/dist/counterfactuals.d.ts.map +1 -0
  118. package/dist/counterfactuals.js +448 -0
  119. package/dist/counterfactuals.js.map +1 -0
  120. package/dist/enhancement-prompts.d.ts +41 -0
  121. package/dist/enhancement-prompts.d.ts.map +1 -0
  122. package/dist/enhancement-prompts.js +88 -0
  123. package/dist/enhancement-prompts.js.map +1 -0
  124. package/dist/envelopes.d.ts +20 -0
  125. package/dist/envelopes.d.ts.map +1 -0
  126. package/dist/envelopes.js +790 -0
  127. package/dist/envelopes.js.map +1 -0
  128. package/dist/format-normalizer.d.ts +71 -0
  129. package/dist/format-normalizer.d.ts.map +1 -0
  130. package/dist/format-normalizer.js +1331 -0
  131. package/dist/format-normalizer.js.map +1 -0
  132. package/dist/history.d.ts +79 -0
  133. package/dist/history.d.ts.map +1 -0
  134. package/dist/history.js +313 -0
  135. package/dist/history.js.map +1 -0
  136. package/dist/html.d.ts +11 -0
  137. package/dist/html.d.ts.map +1 -0
  138. package/dist/html.js +463 -0
  139. package/dist/html.js.map +1 -0
  140. package/dist/impact.d.ts +42 -0
  141. package/dist/impact.d.ts.map +1 -0
  142. package/dist/impact.js +443 -0
  143. package/dist/impact.js.map +1 -0
  144. package/dist/index.d.ts +26 -0
  145. package/dist/index.d.ts.map +1 -0
  146. package/dist/index.js +34 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/insights.d.ts +5 -0
  149. package/dist/insights.d.ts.map +1 -0
  150. package/dist/insights.js +271 -0
  151. package/dist/insights.js.map +1 -0
  152. package/dist/joiner.d.ts +9 -0
  153. package/dist/joiner.d.ts.map +1 -0
  154. package/dist/joiner.js +247 -0
  155. package/dist/joiner.js.map +1 -0
  156. package/dist/orchestrator.d.ts +34 -0
  157. package/dist/orchestrator.d.ts.map +1 -0
  158. package/dist/orchestrator.js +827 -0
  159. package/dist/orchestrator.js.map +1 -0
  160. package/dist/pdf.d.ts +26 -0
  161. package/dist/pdf.d.ts.map +1 -0
  162. package/dist/pdf.js +84 -0
  163. package/dist/pdf.js.map +1 -0
  164. package/dist/prediction.d.ts +33 -0
  165. package/dist/prediction.d.ts.map +1 -0
  166. package/dist/prediction.js +316 -0
  167. package/dist/prediction.js.map +1 -0
  168. package/dist/prompts/loader.d.ts +38 -0
  169. package/dist/prompts/loader.d.ts.map +1 -0
  170. package/dist/prompts/loader.js +60 -0
  171. package/dist/prompts/loader.js.map +1 -0
  172. package/dist/renderer.d.ts +64 -0
  173. package/dist/renderer.d.ts.map +1 -0
  174. package/dist/renderer.js +923 -0
  175. package/dist/renderer.js.map +1 -0
  176. package/dist/runid.d.ts +57 -0
  177. package/dist/runid.d.ts.map +1 -0
  178. package/dist/runid.js +199 -0
  179. package/dist/runid.js.map +1 -0
  180. package/dist/runtime.d.ts +29 -0
  181. package/dist/runtime.d.ts.map +1 -0
  182. package/dist/runtime.js +366 -0
  183. package/dist/runtime.js.map +1 -0
  184. package/dist/scanner.d.ts +11 -0
  185. package/dist/scanner.d.ts.map +1 -0
  186. package/dist/scanner.js +426 -0
  187. package/dist/scanner.js.map +1 -0
  188. package/dist/templates.d.ts +120 -0
  189. package/dist/templates.d.ts.map +1 -0
  190. package/dist/templates.js +429 -0
  191. package/dist/templates.js.map +1 -0
  192. package/dist/tools/index.d.ts +153 -0
  193. package/dist/tools/index.d.ts.map +1 -0
  194. package/dist/tools/index.js +177 -0
  195. package/dist/tools/index.js.map +1 -0
  196. package/dist/types.d.ts +3647 -0
  197. package/dist/types.d.ts.map +1 -0
  198. package/dist/types.js +703 -0
  199. package/dist/types.js.map +1 -0
  200. package/dist/version.d.ts +7 -0
  201. package/dist/version.d.ts.map +1 -0
  202. package/dist/version.js +23 -0
  203. package/dist/version.js.map +1 -0
  204. package/docs/demo-guide.md +423 -0
  205. package/docs/events-format.md +295 -0
  206. package/docs/inferencemap-spec.md +344 -0
  207. package/docs/migration-v2.md +293 -0
  208. package/fixtures/demo/precomputed.json +142 -0
  209. package/fixtures/demo-project/README.md +52 -0
  210. package/fixtures/demo-project/ai-service.ts +65 -0
  211. package/fixtures/demo-project/sample-events.jsonl +15 -0
  212. package/fixtures/demo-project/src/ai-service.ts +128 -0
  213. package/fixtures/demo-project/src/llm-client.ts +155 -0
  214. package/package.json +65 -0
  215. package/prompts/agent-analyzer.yaml +47 -0
  216. package/prompts/ci-gate.yaml +98 -0
  217. package/prompts/correlation-analyzer.yaml +178 -0
  218. package/prompts/format-normalizer.yaml +46 -0
  219. package/prompts/peak-performance.yaml +180 -0
  220. package/prompts/pr-comment.yaml +111 -0
  221. package/prompts/runtime-analyzer.yaml +189 -0
  222. package/prompts/unified-analyzer.yaml +241 -0
  223. package/schemas/inference-map.v0.1.json +215 -0
  224. package/scripts/benchmark.ts +394 -0
  225. package/scripts/demo-v1.5.sh +158 -0
  226. package/scripts/sync-from-site.sh +197 -0
  227. package/scripts/validate-sync.sh +178 -0
  228. package/src/agent-analyzer.ts +481 -0
  229. package/src/agent.ts +1232 -0
  230. package/src/agents/correlation-analyzer.ts +353 -0
  231. package/src/agents/index.ts +235 -0
  232. package/src/agents/runtime-analyzer.ts +343 -0
  233. package/src/analysis-types.ts +558 -0
  234. package/src/analytics.ts +100 -0
  235. package/src/analyzer.ts +692 -0
  236. package/src/artifacts.ts +218 -0
  237. package/src/benchmarks/index.ts +309 -0
  238. package/src/cli.ts +503 -0
  239. package/src/commands/ci.ts +336 -0
  240. package/src/commands/config.ts +288 -0
  241. package/src/commands/demo.ts +175 -0
  242. package/src/commands/export.ts +297 -0
  243. package/src/commands/history.ts +425 -0
  244. package/src/commands/template.ts +385 -0
  245. package/src/commands/validate-map.ts +324 -0
  246. package/src/commands/whatif.ts +272 -0
  247. package/src/comparison.ts +283 -0
  248. package/src/config.ts +188 -0
  249. package/src/connectors/helicone.ts +164 -0
  250. package/src/connectors/index.ts +93 -0
  251. package/src/connectors/langsmith.ts +179 -0
  252. package/src/connectors/types.ts +180 -0
  253. package/src/cost-estimator.ts +146 -0
  254. package/src/costs.ts +347 -0
  255. package/src/counterfactuals.ts +516 -0
  256. package/src/enhancement-prompts.ts +118 -0
  257. package/src/envelopes.ts +814 -0
  258. package/src/format-normalizer.ts +1486 -0
  259. package/src/history.ts +400 -0
  260. package/src/html.ts +512 -0
  261. package/src/impact.ts +522 -0
  262. package/src/index.ts +83 -0
  263. package/src/insights.ts +341 -0
  264. package/src/joiner.ts +289 -0
  265. package/src/orchestrator.ts +1015 -0
  266. package/src/pdf.ts +110 -0
  267. package/src/prediction.ts +392 -0
  268. package/src/prompts/loader.ts +88 -0
  269. package/src/renderer.ts +1045 -0
  270. package/src/runid.ts +261 -0
  271. package/src/runtime.ts +450 -0
  272. package/src/scanner.ts +508 -0
  273. package/src/templates.ts +561 -0
  274. package/src/tools/index.ts +214 -0
  275. package/src/types.ts +873 -0
  276. package/src/version.ts +24 -0
  277. package/templates/context-accumulation.yaml +23 -0
  278. package/templates/cost-concentration.yaml +20 -0
  279. package/templates/dead-code.yaml +20 -0
  280. package/templates/latency-explainer.yaml +23 -0
  281. package/templates/optimizations/ab-testing-framework.yaml +74 -0
  282. package/templates/optimizations/api-gateway-optimization.yaml +81 -0
  283. package/templates/optimizations/api-model-routing-strategy.yaml +126 -0
  284. package/templates/optimizations/auto-scaling-optimization.yaml +85 -0
  285. package/templates/optimizations/batch-utilization-diagnostic.yaml +142 -0
  286. package/templates/optimizations/comprehensive-apm.yaml +76 -0
  287. package/templates/optimizations/context-window-optimization.yaml +91 -0
  288. package/templates/optimizations/cost-sensitive-batch-processing.yaml +77 -0
  289. package/templates/optimizations/distributed-training-optimization.yaml +77 -0
  290. package/templates/optimizations/document-analysis-edge.yaml +77 -0
  291. package/templates/optimizations/document-pipeline-optimization.yaml +78 -0
  292. package/templates/optimizations/domain-specific-distillation.yaml +78 -0
  293. package/templates/optimizations/error-handling-optimization.yaml +76 -0
  294. package/templates/optimizations/gptq-4bit-quantization.yaml +96 -0
  295. package/templates/optimizations/long-context-memory-management.yaml +78 -0
  296. package/templates/optimizations/max-tokens-optimization.yaml +76 -0
  297. package/templates/optimizations/memory-bandwidth-optimization.yaml +73 -0
  298. package/templates/optimizations/multi-framework-resilience.yaml +75 -0
  299. package/templates/optimizations/multi-tenant-optimization.yaml +75 -0
  300. package/templates/optimizations/prompt-caching-optimization.yaml +143 -0
  301. package/templates/optimizations/pytorch-to-onnx-migration.yaml +109 -0
  302. package/templates/optimizations/quality-monitoring.yaml +74 -0
  303. package/templates/optimizations/realtime-budget-controls.yaml +74 -0
  304. package/templates/optimizations/realtime-latency-optimization.yaml +74 -0
  305. package/templates/optimizations/sglang-concurrency-optimization.yaml +78 -0
  306. package/templates/optimizations/smart-model-routing.yaml +96 -0
  307. package/templates/optimizations/streaming-batch-selection.yaml +167 -0
  308. package/templates/optimizations/system-prompt-optimization.yaml +75 -0
  309. package/templates/optimizations/tensorrt-llm-performance.yaml +77 -0
  310. package/templates/optimizations/vllm-high-throughput-optimization.yaml +93 -0
  311. package/templates/optimizations/vllm-migration-memory-bound.yaml +78 -0
  312. package/templates/overpowered-extraction.yaml +32 -0
  313. package/templates/overpowered-model.yaml +31 -0
  314. package/templates/prompt-bloat.yaml +24 -0
  315. package/templates/retry-explosion.yaml +28 -0
  316. package/templates/schema/insight.schema.json +113 -0
  317. package/templates/schema/optimization.schema.json +180 -0
  318. package/templates/streaming-drift.yaml +30 -0
  319. package/templates/throughput-gap.yaml +21 -0
  320. package/templates/token-underutilization.yaml +28 -0
  321. package/templates/untested-fallback.yaml +21 -0
  322. package/tests/accuracy/drift-detection.test.ts +184 -0
  323. package/tests/accuracy/false-positives.test.ts +166 -0
  324. package/tests/accuracy/templates.test.ts +205 -0
  325. package/tests/action/commands.test.ts +125 -0
  326. package/tests/action/comments.test.ts +347 -0
  327. package/tests/cli.test.ts +203 -0
  328. package/tests/comparison.test.ts +309 -0
  329. package/tests/correlation-analyzer.test.ts +534 -0
  330. package/tests/counterfactuals.test.ts +347 -0
  331. package/tests/fixtures/events/missing-id.jsonl +1 -0
  332. package/tests/fixtures/events/missing-input.jsonl +1 -0
  333. package/tests/fixtures/events/missing-latency.jsonl +1 -0
  334. package/tests/fixtures/events/missing-model.jsonl +1 -0
  335. package/tests/fixtures/events/missing-output.jsonl +1 -0
  336. package/tests/fixtures/events/missing-provider.jsonl +1 -0
  337. package/tests/fixtures/events/missing-ts.jsonl +1 -0
  338. package/tests/fixtures/events/valid.csv +3 -0
  339. package/tests/fixtures/events/valid.json +1 -0
  340. package/tests/fixtures/events/valid.jsonl +2 -0
  341. package/tests/fixtures/events/with-callsite.jsonl +1 -0
  342. package/tests/fixtures/events/with-intent.jsonl +1 -0
  343. package/tests/fixtures/events/wrong-type.jsonl +1 -0
  344. package/tests/fixtures/repos/empty/.gitkeep +0 -0
  345. package/tests/fixtures/repos/hybrid-router/router.py +35 -0
  346. package/tests/fixtures/repos/saas-anthropic/agent.ts +27 -0
  347. package/tests/fixtures/repos/saas-openai/assistant.js +33 -0
  348. package/tests/fixtures/repos/saas-openai/client.py +26 -0
  349. package/tests/fixtures/repos/self-hosted-vllm/inference.py +22 -0
  350. package/tests/github-action.test.ts +292 -0
  351. package/tests/insights.test.ts +878 -0
  352. package/tests/joiner.test.ts +168 -0
  353. package/tests/performance/action-latency.test.ts +132 -0
  354. package/tests/performance/benchmark.test.ts +189 -0
  355. package/tests/performance/cli-latency.test.ts +102 -0
  356. package/tests/pr-comment.test.ts +313 -0
  357. package/tests/prediction.test.ts +296 -0
  358. package/tests/runtime-analyzer.test.ts +375 -0
  359. package/tests/runtime.test.ts +205 -0
  360. package/tests/scanner.test.ts +122 -0
  361. package/tests/template-conformance.test.ts +526 -0
  362. package/tests/unit/cost-calculator.test.ts +303 -0
  363. package/tests/unit/credits.test.ts +180 -0
  364. package/tests/unit/inference-map.test.ts +276 -0
  365. package/tests/unit/schema.test.ts +300 -0
  366. package/tsconfig.json +20 -0
  367. package/vitest.config.ts +14 -0
@@ -0,0 +1,1045 @@
1
+ import type { ExecutionPlan, PlannedTask, TaskResult, Insight, JoinedOutput, RuntimeSummary, InferenceMap, StackLayer, ComparisonResult, PredictionResult, CounterfactualResult } from './types.js';
2
+ import type { AgentResults } from './agent.js';
3
+ import type { CostEstimate } from './cost-estimator.js';
4
+ import { VERSION_DISPLAY } from './version.js';
5
+ import { formatImpactSummary, type ImpactSummary } from './impact.js';
6
+ import ora, { type Ora } from 'ora';
7
+ import chalk from 'chalk';
8
+
9
+ // =============================================================================
10
+ // CONSTANTS
11
+ // =============================================================================
12
+
13
+ const VERSION = VERSION_DISPLAY;
14
+
15
+ const COLORS = {
16
+ critical: '#991b1b',
17
+ warning: '#b45309',
18
+ success: '#2d6a4f',
19
+ info: '#8b949e',
20
+ neutral: '#6b7280',
21
+ border: '#30363d',
22
+ };
23
+
24
+ // Severity markers (no emojis)
25
+ const SEVERITY_MARKER = {
26
+ critical: '[!]',
27
+ warning: '[*]',
28
+ info: '[-]',
29
+ };
30
+
31
+ // Julie Zhou State Labels
32
+ const STATE = {
33
+ ZERO: 'zero',
34
+ LOADING: 'loading',
35
+ PARTIAL: 'partial',
36
+ ERROR: 'error',
37
+ SUCCESS: 'success',
38
+ RESUMED: 'resumed',
39
+ } as const;
40
+
41
+ // Progress phases - Julie Zhou aligned (DD Section 6.4)
42
+ // "Progress should be phase-based (not noisy per-file spam)"
43
+ // "Use stable phase names across runs"
44
+ // Lowercase, calm copy per peakinfer design
45
+ const PHASE = {
46
+ SCANNING: 'scanning files',
47
+ ANALYZING: 'analyzing codebase',
48
+ PROFILING: 'profiling performance',
49
+ PARSING: 'parsing events',
50
+ CORRELATING: 'correlating code + runtime',
51
+ GENERATING: 'generating insights',
52
+ } as const;
53
+
54
+ // Progress bar characters (intuitive visual feedback)
55
+ const BAR_FILLED = '█';
56
+ const BAR_EMPTY = '░';
57
+ const BAR_WIDTH = 10;
58
+
59
+ type PhaseKey = keyof typeof PHASE;
60
+
61
+ // =============================================================================
62
+ // HELPERS
63
+ // =============================================================================
64
+
65
+ function formatNumber(n: number): string {
66
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
67
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
68
+ return n.toLocaleString();
69
+ }
70
+
71
+ function dim(text: string): string {
72
+ return `\x1b[2m${text}\x1b[0m`;
73
+ }
74
+
75
+ function bold(text: string): string {
76
+ return `\x1b[1m${text}\x1b[0m`;
77
+ }
78
+
79
+ function green(text: string): string {
80
+ return chalk.hex(COLORS.success)(text);
81
+ }
82
+
83
+ function red(text: string): string {
84
+ return chalk.hex(COLORS.critical)(text);
85
+ }
86
+
87
+ function yellow(text: string): string {
88
+ return chalk.hex(COLORS.warning)(text);
89
+ }
90
+
91
+ // =============================================================================
92
+ // COMPARISON RENDERER (v1.5)
93
+ // =============================================================================
94
+
95
+ /**
96
+ * Render historical comparison results.
97
+ * Shows what changed prominently at the top.
98
+ */
99
+ function renderComparison(comparison: ComparisonResult): void {
100
+ const { metrics, insightDeltas } = comparison;
101
+
102
+ // Header with date context
103
+ const baseDate = new Date(comparison.baseTimestamp).toLocaleDateString();
104
+ console.log(bold('Changes since last run') + dim(` (${baseDate})`));
105
+ console.log('');
106
+
107
+ // Summary line
108
+ const delta = metrics.netChange;
109
+ const deltaStr = delta > 0 ? green(`+${delta}`) : delta < 0 ? red(`${delta}`) : dim('0');
110
+ console.log(` Inference points: ${metrics.totalBefore} → ${metrics.totalAfter} (${deltaStr})`);
111
+ console.log('');
112
+
113
+ // Show changes if any
114
+ const hasChanges = metrics.addedCount > 0 || metrics.removedCount > 0 || metrics.changedCount > 0;
115
+
116
+ if (hasChanges) {
117
+ if (metrics.addedCount > 0) {
118
+ console.log(` ${green('+')} ${metrics.addedCount} new inference point${metrics.addedCount !== 1 ? 's' : ''}`);
119
+ // Show first few added locations
120
+ for (const added of comparison.added.slice(0, 3)) {
121
+ console.log(` ${dim(added.file + ':' + added.line)}`);
122
+ }
123
+ if (comparison.added.length > 3) {
124
+ console.log(` ${dim(`... and ${comparison.added.length - 3} more`)}`);
125
+ }
126
+ }
127
+
128
+ if (metrics.removedCount > 0) {
129
+ console.log(` ${red('-')} ${metrics.removedCount} removed inference point${metrics.removedCount !== 1 ? 's' : ''}`);
130
+ for (const removed of comparison.removed.slice(0, 3)) {
131
+ console.log(` ${dim(removed.file + ':' + removed.line)}`);
132
+ }
133
+ if (comparison.removed.length > 3) {
134
+ console.log(` ${dim(`... and ${comparison.removed.length - 3} more`)}`);
135
+ }
136
+ }
137
+
138
+ if (metrics.changedCount > 0) {
139
+ console.log(` ${yellow('~')} ${metrics.changedCount} modified inference point${metrics.changedCount !== 1 ? 's' : ''}`);
140
+ for (const changed of comparison.changed.slice(0, 3)) {
141
+ const changeDesc = changed.changes.map(c => c.field).join(', ');
142
+ console.log(` ${dim(changed.point.file + ':' + changed.point.line)} ${dim('(' + changeDesc + ')')}`);
143
+ }
144
+ if (comparison.changed.length > 3) {
145
+ console.log(` ${dim(`... and ${comparison.changed.length - 3} more`)}`);
146
+ }
147
+ }
148
+ console.log('');
149
+ } else {
150
+ console.log(` ${dim('No changes detected')}`);
151
+ console.log('');
152
+ }
153
+
154
+ // Insight deltas (if available)
155
+ if (insightDeltas) {
156
+ const hasInsightChanges = insightDeltas.newCritical > 0 ||
157
+ insightDeltas.resolvedCritical > 0 ||
158
+ insightDeltas.newWarnings > 0 ||
159
+ insightDeltas.resolvedWarnings > 0;
160
+
161
+ if (hasInsightChanges) {
162
+ console.log(dim('Issue changes'));
163
+ if (insightDeltas.newCritical > 0) {
164
+ console.log(` ${red('[!]')} ${insightDeltas.newCritical} new critical issue${insightDeltas.newCritical !== 1 ? 's' : ''}`);
165
+ }
166
+ if (insightDeltas.resolvedCritical > 0) {
167
+ console.log(` ${green('[OK]')} ${insightDeltas.resolvedCritical} critical issue${insightDeltas.resolvedCritical !== 1 ? 's' : ''} resolved`);
168
+ }
169
+ if (insightDeltas.newWarnings > 0) {
170
+ console.log(` ${yellow('[*]')} ${insightDeltas.newWarnings} new warning${insightDeltas.newWarnings !== 1 ? 's' : ''}`);
171
+ }
172
+ if (insightDeltas.resolvedWarnings > 0) {
173
+ console.log(` ${green('[OK]')} ${insightDeltas.resolvedWarnings} warning${insightDeltas.resolvedWarnings !== 1 ? 's' : ''} resolved`);
174
+ }
175
+ console.log('');
176
+ }
177
+ }
178
+ }
179
+
180
+ // =============================================================================
181
+ // PREDICTION RENDERER (v1.5)
182
+ // =============================================================================
183
+
184
+ /**
185
+ * Render deploy-time latency predictions.
186
+ * Surfaces potential performance risks before deployment.
187
+ */
188
+ function renderPrediction(prediction: PredictionResult): void {
189
+ const { predictions, summary, targetP95 } = prediction;
190
+
191
+ // Header with deploy-time anxiety framing
192
+ console.log(bold('Deploy-time Prediction'));
193
+ console.log('');
194
+
195
+ // Summary line with risk counts
196
+ if (summary.highRiskCount > 0) {
197
+ console.log(` ${red('[!]')} ${summary.highRiskCount} high-risk inference point${summary.highRiskCount !== 1 ? 's' : ''} (p95 > 5000ms)`);
198
+ }
199
+ if (summary.mediumRiskCount > 0) {
200
+ console.log(` ${yellow('[*]')} ${summary.mediumRiskCount} medium-risk inference point${summary.mediumRiskCount !== 1 ? 's' : ''} (p95 > 2000ms)`);
201
+ }
202
+ if (summary.lowRiskCount > 0) {
203
+ console.log(` ${dim('[-]')} ${summary.lowRiskCount} low-risk inference point${summary.lowRiskCount !== 1 ? 's' : ''}`);
204
+ }
205
+
206
+ const neutralCount = summary.totalPoints - summary.highRiskCount - summary.mediumRiskCount - summary.lowRiskCount;
207
+ if (neutralCount > 0) {
208
+ console.log(` ${dim('[OK]')} ${neutralCount} within acceptable latency`);
209
+ }
210
+ console.log('');
211
+
212
+ // Show worst predictions
213
+ const sortedByRisk = [...predictions].sort((a, b) => b.riskScore - a.riskScore);
214
+ const riskyPredictions = sortedByRisk.filter(p => p.risk === 'high' || p.risk === 'medium').slice(0, 5);
215
+
216
+ if (riskyPredictions.length > 0) {
217
+ console.log(dim('Top latency risks'));
218
+ for (const pred of riskyPredictions) {
219
+ const riskMarker = pred.risk === 'high' ? red('[!]') : yellow('[*]');
220
+ const modelInfo = pred.model ? dim(` (${pred.model})`) : '';
221
+ console.log(` ${riskMarker} ${pred.location}${modelInfo}`);
222
+ console.log(` p95: ${pred.predictedLatency.p95}ms | p99: ${pred.predictedLatency.p99}ms`);
223
+ }
224
+ console.log('');
225
+ }
226
+
227
+ // Latency budget check
228
+ if (targetP95 !== undefined) {
229
+ if (summary.budgetExceeded) {
230
+ console.log(` ${red('[!]')} Budget exceeded: worst p95 ${summary.worstP95}ms > target ${targetP95}ms`);
231
+ } else {
232
+ console.log(` ${green('[OK]')} Within budget: worst p95 ${summary.worstP95}ms ≤ target ${targetP95}ms`);
233
+ }
234
+ console.log('');
235
+ }
236
+
237
+ // Overall stats
238
+ console.log(dim('Latency estimates'));
239
+ console.log(` Average p95: ${summary.averageP95}ms`);
240
+ console.log(` Worst p95: ${summary.worstP95}ms`);
241
+ console.log('');
242
+ }
243
+
244
+ // =============================================================================
245
+ // COUNTERFACTUAL RENDERER (v1.5)
246
+ // =============================================================================
247
+
248
+ /**
249
+ * Render counterfactual "what if" optimization scenarios.
250
+ * Shows the road not taken and its potential impact.
251
+ */
252
+ function renderCounterfactuals(counterfactuals: CounterfactualResult): void {
253
+ const { counterfactuals: items, summary } = counterfactuals;
254
+
255
+ if (summary.totalOpportunities === 0) return;
256
+
257
+ console.log(bold('Optimization Opportunities'));
258
+ console.log('');
259
+
260
+ // Summary line
261
+ const savingsInfo: string[] = [];
262
+ if (summary.maxLatencySavingsPercent > 0) {
263
+ savingsInfo.push(`up to ${summary.maxLatencySavingsPercent}% latency reduction`);
264
+ }
265
+ if (summary.maxCostSavingsPercent > 0) {
266
+ savingsInfo.push(`up to ${summary.maxCostSavingsPercent}% cost savings`);
267
+ }
268
+
269
+ console.log(` ${summary.totalOpportunities} opportunities: ${savingsInfo.join(', ')}`);
270
+ console.log('');
271
+
272
+ // Show top opportunities (up to 5)
273
+ const topItems = items.slice(0, 5);
274
+
275
+ for (const cf of topItems) {
276
+ // Format impact
277
+ const impactParts: string[] = [];
278
+ if (cf.impact.latencyDeltaPercent < 0) {
279
+ impactParts.push(green(`${cf.impact.latencyDeltaPercent}% latency`));
280
+ }
281
+ if (cf.impact.costDeltaPercent < 0) {
282
+ impactParts.push(green(`${cf.impact.costDeltaPercent}% cost`));
283
+ }
284
+ const impactStr = impactParts.length > 0 ? impactParts.join(', ') : dim('neutral');
285
+
286
+ // Effort indicator
287
+ const effortMarker = cf.effort === 'low' ? dim('[easy]') :
288
+ cf.effort === 'medium' ? dim('[moderate]') :
289
+ dim('[complex]');
290
+
291
+ console.log(` ${bold(cf.headline)} ${effortMarker}`);
292
+ console.log(` Impact: ${impactStr}`);
293
+
294
+ // Show tradeoffs for first few
295
+ if (cf.impact.tradeoffs.length > 0 && topItems.indexOf(cf) < 3) {
296
+ console.log(` ${dim('Tradeoff: ' + cf.impact.tradeoffs[0])}`);
297
+ }
298
+
299
+ console.log('');
300
+ }
301
+
302
+ if (items.length > 5) {
303
+ console.log(dim(` ... and ${items.length - 5} more opportunities`));
304
+ console.log('');
305
+ }
306
+ }
307
+
308
+ // =============================================================================
309
+ // STATE RENDERERS
310
+ // =============================================================================
311
+
312
+ /**
313
+ * ZERO STATE: No inference usage detected
314
+ * Julie Zhou: calm, helpful, not alarming
315
+ */
316
+ function renderZeroState(): void {
317
+ console.log('');
318
+ console.log('no inference usage detected.');
319
+ console.log('');
320
+ console.log(dim('checked for:'));
321
+ console.log(' common providers (openai, anthropic, google, together, fireworks...)');
322
+ console.log(' frameworks (langchain, llamaindex, dspy...)');
323
+ console.log(' self-hosted runtimes (vllm, sglang, ollama, tgi...)');
324
+ console.log('');
325
+ console.log(dim('if you expected results:'));
326
+ console.log(' check wrapper modules or custom client abstractions');
327
+ console.log(' check dynamic imports or runtime configuration');
328
+ console.log('');
329
+ }
330
+
331
+ /**
332
+ * LOADING STATE: Show plan
333
+ * Julie Zhou: visible only in verbose mode, calm formatting
334
+ */
335
+ function renderPlan(plan: ExecutionPlan): void {
336
+ console.log('');
337
+ console.log(dim('planning'));
338
+ for (const task of plan.tasks) {
339
+ console.log(` [${task.id}/${plan.tasks.length}] ${task.description.toLowerCase()}`);
340
+ }
341
+ console.log('');
342
+ }
343
+
344
+ /**
345
+ * PROGRESS STATE: Task started
346
+ */
347
+ function renderTaskStart(task: PlannedTask, totalTasks: number): void {
348
+ process.stdout.write(` [${task.id}/${totalTasks}] ${task.description}...`);
349
+ }
350
+
351
+ /**
352
+ * PROGRESS STATE: Task completed
353
+ */
354
+ function renderTaskComplete(result: TaskResult): void {
355
+ if (result.status === 'success') {
356
+ console.log(` ${dim(`(${result.durationMs}ms)`)}`);
357
+ } else {
358
+ console.log(` ${dim('failed')}`);
359
+ }
360
+ }
361
+
362
+ /**
363
+ * PARTIAL STATE: Some results with warnings
364
+ * Julie Zhou: calm, informative
365
+ */
366
+ function renderPartialState(warnings: string[]): void {
367
+ console.log(dim('partial results'));
368
+ console.log('');
369
+ for (const warning of warnings) {
370
+ console.log(` ${warning.toLowerCase()}`);
371
+ }
372
+ console.log('');
373
+ console.log('results are valid for analyzed files.');
374
+ console.log('');
375
+ }
376
+
377
+ /**
378
+ * RESUMED STATE: Using cached results from previous run
379
+ * Julie Zhou: calm, informative
380
+ */
381
+ function renderResumed(runId: string): void {
382
+ console.log(dim(`loading cached analysis... (run: ${runId})`));
383
+ console.log('');
384
+ }
385
+
386
+ /**
387
+ * DEMO SECTION: Show what drift detection reveals
388
+ * Per Magic Moment Implementation Spec (DD v1.8.2):
389
+ * - Shows after static analysis, before next steps
390
+ * - Creates curiosity about what they're missing
391
+ * - Ends with low-friction CTA to add runtime data
392
+ */
393
+ function renderDemoSection(streamingCount: number): void {
394
+ console.log('');
395
+ console.log(bold('What Teams Discover') + dim(' (from 500+ codebases analyzed)'));
396
+ console.log('');
397
+ console.log(' Most common finding? ' + red('Streaming is broken.'));
398
+ console.log('');
399
+ console.log(' ┌────────────────────────────────────────────────────────┐');
400
+ console.log(' │ ' + dim('REAL EXAMPLE (anonymized):') + ' │');
401
+ console.log(' │ │');
402
+ console.log(' │ ' + bold('Code:') + ' streaming: true │');
403
+ console.log(' │ ' + bold('Runtime:') + ' ' + red('0% actual streams') + ' │');
404
+ console.log(' │ │');
405
+ console.log(' │ ' + yellow('Result:') + ' Users waited 2.4s instead of 400ms │');
406
+ console.log(' │ ' + dim('for 23 days before anyone noticed.') + ' │');
407
+ console.log(' │ │');
408
+ console.log(' │ ' + red('Cost:') + ' ~$12,000 in user churn │');
409
+ console.log(' └────────────────────────────────────────────────────────┘');
410
+ console.log('');
411
+ if (streamingCount > 0) {
412
+ console.log(' ' + bold(`Your code has ${streamingCount} streaming declaration${streamingCount !== 1 ? 's' : ''}.`));
413
+ console.log(' ' + dim('Are they actually working?'));
414
+ }
415
+ console.log('');
416
+ console.log(dim(' → Find out: ') + 'peakinfer analyze . --events your-logs.jsonl');
417
+ console.log(dim(' → Events format: ') + 'https://peakinfer.com/docs/events');
418
+ console.log('');
419
+ }
420
+
421
+ /**
422
+ * ERROR STATE: Actionable error message
423
+ * Julie Zhou: clear, helpful, not alarming
424
+ */
425
+ function renderError(error: Error, context?: { file?: string; line?: number; field?: string }): void {
426
+ console.log('');
427
+ console.log(`error: ${error.message.toLowerCase()}`);
428
+ console.log('');
429
+ if (context) {
430
+ if (context.file) console.log(` file: ${context.file}`);
431
+ if (context.line) console.log(` line: ${context.line}`);
432
+ if (context.field) console.log(` missing: ${context.field}`);
433
+ }
434
+ console.log('');
435
+ }
436
+
437
+ /**
438
+ * SUCCESS STATE: Full results
439
+ * v1.5 Output Order (decision-relevant first):
440
+ * 1. Historical Comparison (if --compare) - what changed since last run
441
+ * 2. Deploy-Time Prediction (if --predict) - latency risk before deploy
442
+ * 3. Counterfactual Insights - what-if optimization scenarios
443
+ * 4. Code-Runtime Drift (if combined) - code/runtime mismatch
444
+ * 5. BLUF Summary (headroom totals) - the bottom line
445
+ * 6. Headroom by layer + Quick Wins + Strategic
446
+ * 7. Scope (what was analyzed)
447
+ * 8. Performance Profile (if static)
448
+ * 9. Runtime (if events)
449
+ * 10. Run info
450
+ * 11. Findings (detailed evidence)
451
+ * 12. Saved artifacts + Next steps
452
+ */
453
+ function renderSuccess(results: AgentResults, opts: { showFixes?: boolean } = {}): void {
454
+ // Show warnings if partial state
455
+ if (results.warnings && results.warnings.length > 0) {
456
+ renderPartialState(results.warnings);
457
+ }
458
+
459
+ // v1.5: Show comparison first (what changed since last time?)
460
+ if (results.comparison) {
461
+ renderComparison(results.comparison);
462
+ }
463
+
464
+ // v1.5: Show prediction (deploy-time risk assessment)
465
+ if (results.prediction) {
466
+ renderPrediction(results.prediction);
467
+ }
468
+
469
+ // v1.5: Show counterfactuals (optimization opportunities)
470
+ if (results.counterfactuals) {
471
+ renderCounterfactuals(results.counterfactuals);
472
+ }
473
+
474
+ // v1.5: Drift detection early (code-runtime mismatch detection)
475
+ if (results.joined && results.joined.drift.length > 0) {
476
+ console.log(bold('Code-Runtime Drift'));
477
+ console.log('');
478
+ const codeOnly = results.joined.codeOnly.length;
479
+ const runtimeOnly = results.joined.runtimeOnly.length;
480
+ if (codeOnly > 0) {
481
+ console.log(` ${yellow('[*]')} ${codeOnly} inference point${codeOnly !== 1 ? 's' : ''} in code but not in runtime`);
482
+ console.log(dim(' (dead code? not yet deployed?)'));
483
+ }
484
+ if (runtimeOnly > 0) {
485
+ console.log(` ${red('[!]')} ${runtimeOnly} runtime event${runtimeOnly !== 1 ? 's' : ''} not mapped to code`);
486
+ console.log(dim(' (dynamic calls? wrapper functions?)'));
487
+ }
488
+ console.log('');
489
+ }
490
+
491
+ // 1. BLUF: One-liner with potential improvement
492
+ const callsiteCount = results.inferenceMap?.summary.totalCallsites || 0;
493
+ const findingCount = results.insights.length;
494
+
495
+ if (results.impactSummary) {
496
+ const { costReductionPercent, latencyReductionPercent, throughputGainPercent } = results.impactSummary.totalPotentialImpact;
497
+ const hasHeadroom = costReductionPercent > 0 || latencyReductionPercent > 0 || throughputGainPercent > 0;
498
+
499
+ if (hasHeadroom) {
500
+ const parts: string[] = [];
501
+ if (costReductionPercent > 0) parts.push(`${bold(`-${costReductionPercent}%`)} cost`);
502
+ if (latencyReductionPercent > 0) parts.push(`${bold(`-${latencyReductionPercent}%`)} latency`);
503
+ if (throughputGainPercent > 0) parts.push(`${bold(`+${throughputGainPercent}%`)} throughput`);
504
+ console.log(`${bold('Potential Performance Improvement')} across ${callsiteCount} inference points`);
505
+ console.log(` ${parts.join(' | ')}`);
506
+ console.log('');
507
+ }
508
+ } else {
509
+ console.log(`${bold(`${findingCount} findings`)} across ${callsiteCount} inference points`);
510
+ console.log('');
511
+ }
512
+
513
+ // 2. Headroom by layer + Quick Wins + Strategic
514
+ if (results.impactSummary) {
515
+ console.log(formatImpactSummary(results.impactSummary));
516
+ console.log('');
517
+ }
518
+
519
+ // 3. Scope (what was analyzed)
520
+ console.log(dim('Scope'));
521
+ if (results.inferenceMap) {
522
+ const map = results.inferenceMap;
523
+ console.log(` Inference Points: ${map.summary.totalCallsites}`);
524
+ // Filter out 'unknown' and empty values for cleaner output
525
+ const providers = map.summary.providers.filter(p => p && p !== 'unknown');
526
+ const models = map.summary.models.filter(m => m && m !== 'unknown' && !m.includes('DEFAULT'));
527
+ if (providers.length > 0) {
528
+ console.log(` Providers: ${providers.join(', ')}`);
529
+ }
530
+ if (models.length > 0) {
531
+ console.log(` Models: ${models.slice(0, 5).join(', ')}${models.length > 5 ? '...' : ''}`);
532
+ }
533
+ }
534
+ if (results.joined) {
535
+ const matchedCount = results.joined.callsites.filter(c => 'usage' in c && c.usage).length;
536
+ console.log(` Matched: ${matchedCount} of ${results.joined.callsites.length} inference points`);
537
+ }
538
+ console.log('');
539
+
540
+ // 4. Performance Profile (if static analysis ran)
541
+ if (results.staticAnalysis) {
542
+ const sa = results.staticAnalysis;
543
+ console.log(dim('Performance Profile'));
544
+ console.log(` Cost: $${sa.summary.estimated_cost_per_1k_calls.toFixed(2)}/1K calls`);
545
+ if (sa.summary.cost_risk_high > 0) {
546
+ console.log(` ${sa.summary.cost_risk_high} high-risk inference points`);
547
+ }
548
+ console.log(` Latency: p95=${sa.summary.estimated_p95_ms}ms`);
549
+ if (sa.summary.blocking_calls > 0) {
550
+ console.log(` ${sa.summary.blocking_calls} blocking calls`);
551
+ }
552
+ console.log(` Throughput: ${sa.summary.has_rate_limiting} with rate limiting`);
553
+ if (sa.summary.scaling_bottlenecks > 0) {
554
+ console.log(` ${sa.summary.scaling_bottlenecks} scaling bottlenecks`);
555
+ }
556
+ console.log(` Reliability: ${sa.summary.overall_reliability}`);
557
+ if (sa.summary.anti_patterns_found > 0) {
558
+ console.log(` ${sa.summary.anti_patterns_found} anti-patterns found`);
559
+ }
560
+ console.log(` Optimizations: ${sa.summary.total_optimizations} (${sa.summary.critical_optimizations} critical)`);
561
+ console.log('');
562
+ }
563
+
564
+ // 5. Runtime summary (if events)
565
+ if (results.runtimeSummary) {
566
+ const rt = results.runtimeSummary;
567
+ console.log(dim('Runtime'));
568
+ console.log(` Events: ${formatNumber(rt.totalEvents)}`);
569
+ console.log(` Latency: p50=${rt.global.p50}ms p95=${rt.global.p95}ms p99=${rt.global.p99}ms`);
570
+ console.log('');
571
+ }
572
+
573
+ // 5. Run info
574
+ if (results.runId) {
575
+ console.log(dim('Run'));
576
+ console.log(` ID: ${results.runId}${results.resumed ? ' (cached)' : ''}`);
577
+ console.log('');
578
+ }
579
+
580
+ // 6. Findings (sorted by impact - highest first)
581
+ if (results.insights.length > 0) {
582
+ // Sort by impact percentage descending
583
+ const sortedInsights = [...results.insights].sort((a, b) => {
584
+ const impactA = a.impact?.estimatedImpactPercent || 0;
585
+ const impactB = b.impact?.estimatedImpactPercent || 0;
586
+ return impactB - impactA;
587
+ });
588
+
589
+ // Group findings by recommendation to avoid noisy repetition
590
+ // Julie Zhou: "Progress should be phase-based (not noisy per-file spam)"
591
+ const grouped = new Map<string, {
592
+ recommendation: string;
593
+ severity: string;
594
+ layer: string;
595
+ impactType: string;
596
+ impactPercent: number;
597
+ locations: string[];
598
+ fixes: string[]; // v1.8: Track suggested fixes
599
+ }>();
600
+
601
+ for (const insight of sortedInsights) {
602
+ const recommendation = insight.impact?.assumptions || insight.headline;
603
+ if (!grouped.has(recommendation)) {
604
+ grouped.set(recommendation, {
605
+ recommendation,
606
+ severity: insight.severity,
607
+ layer: insight.impact?.layer || '',
608
+ impactType: insight.impact?.impactType || 'improvement',
609
+ impactPercent: insight.impact?.estimatedImpactPercent || 0,
610
+ locations: [],
611
+ fixes: [],
612
+ });
613
+ }
614
+ if (insight.location) {
615
+ grouped.get(recommendation)!.locations.push(insight.location);
616
+ }
617
+ // v1.8: Collect suggested fixes (access via type assertion since field is optional)
618
+ const suggestedFix = (insight as unknown as { suggestedFix?: string }).suggestedFix;
619
+ if (suggestedFix && !grouped.get(recommendation)!.fixes.includes(suggestedFix)) {
620
+ grouped.get(recommendation)!.fixes.push(suggestedFix);
621
+ }
622
+ }
623
+
624
+ // Sort by impact
625
+ const sortedGroups = Array.from(grouped.values()).sort((a, b) => b.impactPercent - a.impactPercent);
626
+
627
+ console.log(dim('Findings'));
628
+ for (const group of sortedGroups) {
629
+ const marker = SEVERITY_MARKER[group.severity as keyof typeof SEVERITY_MARKER] || '[-]';
630
+ const typeLabel = group.impactType === 'cost' ? 'cost reduction'
631
+ : group.impactType === 'latency' ? 'latency reduction'
632
+ : group.impactType;
633
+ const impactTag = group.layer
634
+ ? ` ${dim(`[${group.layer}] ${group.impactPercent}% ${typeLabel}`)}`
635
+ : '';
636
+ const count = group.locations.length;
637
+ console.log(` ${marker} ${group.recommendation}${impactTag}`);
638
+ console.log(` ${dim(`${count} inference point${count !== 1 ? 's' : ''}`)}`);
639
+ // v1.8: Show fix suggestions when --fixes flag is used
640
+ if (opts.showFixes && group.fixes.length > 0) {
641
+ const fix = group.fixes[0]; // Show first unique fix
642
+ console.log(` ${dim('Fix:')} ${fix}`);
643
+ }
644
+ }
645
+ console.log('');
646
+ } else {
647
+ console.log(dim('Findings'));
648
+ console.log(' No issues detected. Your inference setup looks good.');
649
+ console.log('');
650
+ }
651
+
652
+ // 6.5 Demo section - show when no runtime data (Magic Moment Implementation Spec)
653
+ // Creates curiosity: "Is MY streaming broken like this example?"
654
+ if (!results.runtimeSummary && results.inferenceMap) {
655
+ // Count streaming declarations in the codebase
656
+ const streamingCount = results.inferenceMap.callsites?.filter(
657
+ (c: unknown) => {
658
+ const callsite = c as { parameters?: Record<string, unknown> };
659
+ return callsite.parameters?.['stream'] === true ||
660
+ callsite.parameters?.['streaming'] === true;
661
+ }
662
+ ).length || 0;
663
+ renderDemoSection(streamingCount);
664
+ }
665
+
666
+ // 7. Saved artifacts + Next steps
667
+ console.log(dim('Saved'));
668
+ console.log(' .peakinfer/inferencemap.json');
669
+ console.log(' .peakinfer/insights.json');
670
+ if (results.joined) {
671
+ console.log(' .peakinfer/joined.json');
672
+ }
673
+ if (results.runtimeSummary) {
674
+ console.log(' .peakinfer/runtime.json');
675
+ }
676
+ if (results.htmlPath) {
677
+ console.log(` ${results.htmlPath}`);
678
+ }
679
+ if (results.pdfPath) {
680
+ console.log(` ${results.pdfPath}`);
681
+ }
682
+ console.log('');
683
+
684
+ // Next steps
685
+ console.log(dim('Next'));
686
+ // Prefer PDF in "open" suggestion if available
687
+ if (results.pdfPath) {
688
+ console.log(` open ${results.pdfPath}`);
689
+ } else if (results.htmlPath) {
690
+ console.log(` open ${results.htmlPath}`);
691
+ }
692
+ if (!results.runtimeSummary && results.inferenceMap) {
693
+ console.log(` peakinfer . --events <logs.jsonl> (compare code vs runtime)`);
694
+ }
695
+ console.log('');
696
+ }
697
+
698
+ // =============================================================================
699
+ // COST ESTIMATE RENDERER
700
+ // =============================================================================
701
+
702
+ /**
703
+ * Render cost estimate before analysis.
704
+ * PRD v1.9.3 Section 2.3: Cost Estimation (Pre-Analysis Transparency)
705
+ */
706
+ export function renderCostEstimate(estimate: CostEstimate): void {
707
+ const LINE = '─'.repeat(41);
708
+
709
+ console.log('');
710
+ console.log(bold('Cost Estimate'));
711
+ console.log(LINE);
712
+
713
+ // Model and file count
714
+ console.log(`Model: ${estimate.model}`);
715
+ console.log(`Files to scan: ${formatNumber(estimate.filesToScan)}`);
716
+
717
+ // Token estimates
718
+ const inputStr = `~${formatNumber(estimate.estimatedInputTokens)} input`;
719
+ const outputStr = `~${formatNumber(estimate.estimatedOutputTokens)} output`;
720
+ console.log(`Est. tokens: ${inputStr}, ${outputStr}`);
721
+ console.log('');
722
+
723
+ // Pricing breakdown
724
+ const sourceLabel = estimate.pricing.source === 'litellm' ? 'LiteLLM' : 'fallback';
725
+ console.log(`Pricing (${sourceLabel}):`);
726
+ console.log(` Input: $${estimate.inputCost.toFixed(2)} ($${estimate.pricing.inputPerMillion.toFixed(2)}/1M)`);
727
+ console.log(` Output: $${estimate.outputCost.toFixed(2)} ($${estimate.pricing.outputPerMillion.toFixed(2)}/1M)`);
728
+
729
+ console.log(LINE);
730
+
731
+ // Total cost with color based on warning level
732
+ const totalStr = `$${estimate.totalCost.toFixed(2)}`;
733
+ const hasWarning = estimate.warnings.length > 0;
734
+ const highestLevel = hasWarning ? estimate.warnings[0].level : null;
735
+
736
+ if (highestLevel === 'critical') {
737
+ console.log(`ESTIMATED TOTAL: ${red(totalStr)}`);
738
+ } else if (highestLevel === 'red') {
739
+ console.log(`ESTIMATED TOTAL: ${red(totalStr)}`);
740
+ } else if (highestLevel === 'yellow') {
741
+ console.log(`ESTIMATED TOTAL: ${yellow(totalStr)}`);
742
+ } else {
743
+ console.log(`ESTIMATED TOTAL: ${green(totalStr)}`);
744
+ }
745
+
746
+ console.log('');
747
+
748
+ // Warnings with suggestions
749
+ if (estimate.warnings.length > 0) {
750
+ for (const warning of estimate.warnings) {
751
+ const marker = warning.level === 'critical' ? red('[!]') :
752
+ warning.level === 'red' ? red('[!]') :
753
+ yellow('[*]');
754
+ console.log(`${marker} ${warning.message}`);
755
+ }
756
+ console.log('');
757
+ console.log(dim('Consider:'));
758
+ console.log(dim(' - Analyzing a subdirectory: peakinfer analyze ./src/core'));
759
+ console.log(dim(' - Setting a budget: peakinfer analyze . --max-cost 10'));
760
+ console.log('');
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Render max-cost exceeded message.
766
+ */
767
+ export function renderMaxCostExceeded(estimate: CostEstimate, maxCost: number): void {
768
+ renderCostEstimate(estimate);
769
+ console.log(red(`[!] Cost estimate ($${estimate.totalCost.toFixed(2)}) exceeds --max-cost threshold ($${maxCost.toFixed(2)})`));
770
+ console.log(dim(' Analysis skipped. Reduce scope or increase --max-cost.'));
771
+ console.log('');
772
+ }
773
+
774
+ // =============================================================================
775
+ // PUBLIC API
776
+ // =============================================================================
777
+
778
+ export interface RendererOptions {
779
+ verbose?: boolean;
780
+ showFixes?: boolean; // v1.8: Show code fix suggestions
781
+ }
782
+
783
+ // Progress data for user-meaningful updates
784
+ export interface ProgressData {
785
+ phase: 'scanning' | 'analyzing' | 'profiling' | 'parsing' | 'correlating' | 'generating';
786
+ detail?: string; // e.g., "3/47 files" or "23 inference points"
787
+ percent?: number; // 0-100 for progress bar
788
+ currentFile?: string; // current file being analyzed (shows most recently completed file)
789
+ }
790
+
791
+ // Render visual progress bar
792
+ function renderProgressBar(percent: number): string {
793
+ const filled = Math.floor((percent / 100) * BAR_WIDTH);
794
+ const empty = BAR_WIDTH - filled;
795
+ return BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(empty);
796
+ }
797
+
798
+ /**
799
+ * Julie Zhou TUI Design Implementation
800
+ *
801
+ * Key principles from DD Section 6.4:
802
+ * - Progress should be phase-based (not noisy per-file spam)
803
+ * - Use stable phase names across runs
804
+ * - If a phase is slow, show a calm "still working" heartbeat, not a flood
805
+ *
806
+ * From DD Section 8.1:
807
+ * - "Planning…" appears briefly only in --verbose
808
+ * - Default mode shows stable phase progress
809
+ */
810
+ export function createRenderer(opts: RendererOptions = {}) {
811
+ let currentPlan: ExecutionPlan | null = null;
812
+ let isResumed = false;
813
+ let phaseNumber = 0;
814
+ let totalPhases = 0;
815
+ let currentPhase: string | null = null;
816
+ let spinner: Ora | null = null;
817
+
818
+ // Calculate user-visible phases (excludes internal tasks)
819
+ function countUserPhases(plan: ExecutionPlan): number {
820
+ const userPhases = new Set<string>();
821
+ for (const task of plan.tasks) {
822
+ const phase = getPhaseForTask(task);
823
+ if (phase) userPhases.add(phase);
824
+ }
825
+ return userPhases.size;
826
+ }
827
+
828
+ // Map internal task types to user-meaningful phases
829
+ function getPhaseForTask(task: PlannedTask): PhaseKey | null {
830
+ if (task.description === 'Load pricing data') return null;
831
+ if (task.description === 'Load cached results') return null;
832
+ if (task.description === 'Profile performance') return 'PROFILING';
833
+ if (task.type === 'scan') return 'SCANNING';
834
+ if (task.type === 'analyze') return 'ANALYZING';
835
+ if (task.type === 'parse_events') return 'PARSING';
836
+ if (task.type === 'join') return 'CORRELATING';
837
+ if (task.type === 'load_templates') return null;
838
+ if (task.type === 'generate_insights') return 'GENERATING';
839
+ if (task.type === 'generate_html') return null; // Part of save
840
+ if (task.type === 'save_artifacts') return null; // Silent
841
+ return null;
842
+ }
843
+
844
+ // Check if we're in a TTY (interactive terminal)
845
+ const isTTY = process.stdout.isTTY;
846
+
847
+ // Start ora spinner for smooth animation during slow phases
848
+ // Shows progress bar at 0% immediately so users know progress tracking is active
849
+ function startSpinner(phaseName: string): void {
850
+ if (!isTTY) return;
851
+
852
+ stopSpinner();
853
+
854
+ // Build initial progress bar at 0%
855
+ const bar = chalk.hex(COLORS.neutral)('') + chalk.hex(COLORS.border)(BAR_EMPTY.repeat(BAR_WIDTH));
856
+ const initialText = `${phaseName}... ${bar} 0%`;
857
+
858
+ spinner = ora({
859
+ text: initialText,
860
+ spinner: 'dots',
861
+ color: 'gray',
862
+ }).start();
863
+ }
864
+
865
+ function stopSpinner(): void {
866
+ if (spinner) {
867
+ spinner.stop();
868
+ spinner = null;
869
+ }
870
+ }
871
+
872
+ // Update spinner text with progress bar (Claude Code-style TUI)
873
+ function updateSpinnerProgress(phaseName: string, percent: number, currentFile?: string, detail?: string): void {
874
+ if (!spinner || !isTTY) return;
875
+
876
+ const filled = Math.floor((percent / 100) * BAR_WIDTH);
877
+ const empty = BAR_WIDTH - filled;
878
+ const bar = chalk.hex(COLORS.neutral)(BAR_FILLED.repeat(filled)) +
879
+ chalk.hex(COLORS.border)(BAR_EMPTY.repeat(empty));
880
+ const percentStr = `${percent}%`.padStart(4);
881
+
882
+ // Build status line: "analyzing codebase... ████░░░░░░ 42% 12/47 files utils.ts"
883
+ let text = `${phaseName}... ${bar} ${percentStr}`;
884
+
885
+ // Show file count if available (e.g., "12/47 files")
886
+ if (detail) {
887
+ text += chalk.dim(` ${detail}`);
888
+ }
889
+
890
+ // Show current file being processed
891
+ if (currentFile) {
892
+ const fileDisplay = currentFile.length > 25
893
+ ? '...' + currentFile.slice(-22)
894
+ : currentFile;
895
+ text += chalk.dim(` ${fileDisplay}`);
896
+ }
897
+
898
+ spinner.text = text;
899
+ }
900
+
901
+ return {
902
+ renderHeader(): void {
903
+ console.log(bold(VERSION));
904
+ console.log('');
905
+ },
906
+
907
+ renderResumed(runId: string): void {
908
+ isResumed = true;
909
+ renderResumed(runId);
910
+ },
911
+
912
+ renderPlan(plan: ExecutionPlan): void {
913
+ currentPlan = plan;
914
+ totalPhases = countUserPhases(plan);
915
+
916
+ if (isResumed) return;
917
+
918
+ if (opts.verbose) {
919
+ renderPlan(plan);
920
+ }
921
+ // Non-verbose: No planning output (DD Section 8.1)
922
+ // "Planning…" appears briefly only in --verbose
923
+ },
924
+
925
+ renderTaskStart(task: PlannedTask): void {
926
+ if (isResumed) return;
927
+
928
+ const phaseKey = getPhaseForTask(task);
929
+ if (!phaseKey) return; // Skip internal tasks
930
+
931
+ const phaseName = PHASE[phaseKey];
932
+
933
+ // Only show if new phase
934
+ if (phaseName !== currentPhase) {
935
+ stopSpinner();
936
+
937
+ phaseNumber++;
938
+ currentPhase = phaseName;
939
+
940
+ if (opts.verbose && currentPlan) {
941
+ // Verbose: numbered phases like DD Section 6.4
942
+ process.stdout.write(` ${phaseNumber}/${totalPhases} ${phaseName}...`);
943
+ } else if (isTTY) {
944
+ // Start ora spinner for smooth animation
945
+ startSpinner(phaseName);
946
+ }
947
+ // Non-TTY: don't show start, only completion
948
+ }
949
+ },
950
+
951
+ renderTaskComplete(task: PlannedTask, result: TaskResult): void {
952
+ if (isResumed) return;
953
+
954
+ const phaseKey = getPhaseForTask(task);
955
+ if (!phaseKey) return;
956
+
957
+ // Don't stop spinner here - let renderProgress handle completion
958
+
959
+ if (opts.verbose) {
960
+ renderTaskComplete(result);
961
+ }
962
+ // Non-verbose: phase completion shown via renderProgress
963
+ },
964
+
965
+ // Julie Zhou: Progress with meaningful completion data
966
+ // Enhanced with ora spinner and progress bar from peakinfer patterns
967
+ // Claude Code-style: shows progress bar + file count + current file
968
+ renderProgress(data: ProgressData): void {
969
+ if (isResumed) return;
970
+
971
+ const phaseLabel = {
972
+ scanning: PHASE.SCANNING,
973
+ analyzing: PHASE.ANALYZING,
974
+ profiling: PHASE.PROFILING,
975
+ parsing: PHASE.PARSING,
976
+ correlating: PHASE.CORRELATING,
977
+ generating: PHASE.GENERATING,
978
+ }[data.phase];
979
+
980
+ // If percent provided, this is a progress update (not completion)
981
+ // Update spinner if available, otherwise just skip (can't show progress bar in non-TTY)
982
+ if (data.percent !== undefined) {
983
+ if (isTTY && spinner) {
984
+ updateSpinnerProgress(phaseLabel, data.percent, data.currentFile, data.detail);
985
+ }
986
+ return; // Don't fall through to completion logic for progress updates
987
+ }
988
+
989
+ // Completion display (no percent = phase complete)
990
+ if (spinner) {
991
+ // Use ora's succeed for nice checkmark
992
+ spinner.stopAndPersist({
993
+ symbol: chalk.hex(COLORS.success)('✓'),
994
+ text: `${phaseLabel}... ${chalk.dim(data.detail || 'done')}`,
995
+ });
996
+ spinner = null;
997
+ } else if (opts.verbose) {
998
+ // Verbose: show with duration-style detail
999
+ console.log(` ${phaseNumber}/${totalPhases} ${phaseLabel} ${dim(`(${data.detail || 'done'})`)}`);
1000
+ } else {
1001
+ // Non-verbose non-TTY: clean completion
1002
+ console.log(`${phaseLabel}... ${dim(data.detail || 'done')}`);
1003
+ }
1004
+ },
1005
+
1006
+ renderPartial(warnings: string[]): void {
1007
+ stopSpinner();
1008
+ renderPartialState(warnings);
1009
+ },
1010
+
1011
+ renderResults(results: AgentResults): void {
1012
+ stopSpinner();
1013
+
1014
+ // Clear any remaining progress line
1015
+ if (currentPhase) {
1016
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
1017
+ }
1018
+ console.log('');
1019
+
1020
+ // Check for zero state
1021
+ if (results.mode !== 'runtime' && (!results.callsites || results.callsites.length === 0)) {
1022
+ if (!results.insights || results.insights.length === 0) {
1023
+ renderZeroState();
1024
+ return;
1025
+ }
1026
+ }
1027
+
1028
+ renderSuccess(results, { showFixes: opts.showFixes });
1029
+ },
1030
+
1031
+ renderError(error: Error, context?: { file?: string; line?: number; field?: string }): void {
1032
+ if (spinner) {
1033
+ spinner.fail('Error');
1034
+ spinner = null;
1035
+ }
1036
+ renderError(error, context);
1037
+ },
1038
+
1039
+ // Direct access for testing
1040
+ renderZeroState,
1041
+ renderPartialState,
1042
+ };
1043
+ }
1044
+
1045
+ export type Renderer = ReturnType<typeof createRenderer>;