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