@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
package/dist/agent.js ADDED
@@ -0,0 +1,965 @@
1
+ import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { parseEvents, aggregate } from './runtime.js';
4
+ import { loadTemplates, getDefaultPrompt } from './templates.js';
5
+ import { loadPricing } from './costs.js';
6
+ import { saveArtifacts, checkResumable, loadArtifacts, generateRunId } from './artifacts.js';
7
+ import { generateHTML } from './html.js';
8
+ import { generatePDF } from './pdf.js';
9
+ import { VERSION } from './version.js';
10
+ import { enrichInsightsWithImpact, generateImpactSummary } from './impact.js';
11
+ import { saveRun, getLatestRun, loadRun } from './history.js';
12
+ import { compareSnapshots, formatComparisonSummary } from './comparison.js';
13
+ import { generatePredictions } from './prediction.js';
14
+ import { generateCounterfactuals } from './counterfactuals.js';
15
+ // Agent SDK pattern (DESIGN.md v2.0 Section 2.1, Patterns v0.2)
16
+ import { DiscoveryAgent, AnalyzerAgent, JoinerAgent, InsightAgent, RuntimeAnalyzerAgent, CorrelationAnalyzerAgent, StaticAnalysisOrchestrator, } from './agents/index.js';
17
+ import { getPricingContext } from './costs.js';
18
+ // =============================================================================
19
+ // HELPERS
20
+ // =============================================================================
21
+ /**
22
+ * Create synthetic enriched inference points from runtime events for insight evaluation.
23
+ * Groups events by provider:model and computes usage statistics.
24
+ * This enables runtime-only analysis to benefit from template-based insights.
25
+ */
26
+ function createSyntheticCallsitesFromEvents(events) {
27
+ // Group events by provider:model
28
+ const groups = new Map();
29
+ for (const event of events) {
30
+ const key = `${event.provider}:${event.model}`;
31
+ if (!groups.has(key)) {
32
+ groups.set(key, []);
33
+ }
34
+ groups.get(key).push(event);
35
+ }
36
+ // Convert each group to a synthetic enriched inference point
37
+ const callsites = [];
38
+ let id = 1;
39
+ for (const [key, groupEvents] of groups) {
40
+ const [provider, model] = key.split(':');
41
+ // Compute usage stats
42
+ const calls = groupEvents.length;
43
+ const tokens_in = groupEvents.reduce((sum, e) => sum + e.input_tokens, 0);
44
+ const tokens_out = groupEvents.reduce((sum, e) => sum + e.output_tokens, 0);
45
+ const latencies = groupEvents.map(e => e.latency_ms).sort((a, b) => a - b);
46
+ const p50Index = Math.floor(latencies.length * 0.5);
47
+ const p95Index = Math.floor(latencies.length * 0.95);
48
+ const p99Index = Math.floor(latencies.length * 0.99);
49
+ callsites.push({
50
+ id: `runtime-${id++}`,
51
+ file: 'runtime', // Synthetic location
52
+ line: 0,
53
+ provider: provider,
54
+ model,
55
+ framework: null,
56
+ runtime: null,
57
+ patterns: {
58
+ streaming: groupEvents.some(e => e.streaming === true),
59
+ batching: groupEvents.some(e => e.batch_id !== undefined),
60
+ caching: groupEvents.some(e => e.cached === true),
61
+ retries: groupEvents.some(e => (e.retry_count || 0) > 0),
62
+ fallback: groupEvents.some(e => e.fallback_used === true),
63
+ },
64
+ confidence: 1.0,
65
+ usage: {
66
+ calls,
67
+ tokens_in,
68
+ tokens_out,
69
+ latency_p50: latencies[p50Index] || 0,
70
+ latency_p95: latencies[p95Index] || latencies[p50Index] || 0,
71
+ latency_p99: latencies[p99Index] || latencies[p95Index] || latencies[p50Index] || 0,
72
+ },
73
+ });
74
+ }
75
+ return callsites;
76
+ }
77
+ /**
78
+ * Convert StaticAnalysisOutput to LLM insights for display.
79
+ * Maps optimizations from all dimensions (cost, latency, throughput, reliability) to insights.
80
+ */
81
+ function convertPerformanceToInsights(analysis) {
82
+ const insights = [];
83
+ // Convert all optimizations to insights
84
+ for (const opt of analysis.all_optimizations) {
85
+ const severity = opt.priority === 'critical' || opt.priority === 'high' ? 'critical' :
86
+ opt.priority === 'medium' ? 'warning' : 'info';
87
+ const categoryMap = {
88
+ cost: 'Cost Optimization',
89
+ latency: 'Latency Optimization',
90
+ throughput: 'Throughput Optimization',
91
+ reliability: 'Reliability Improvement',
92
+ };
93
+ const impactTypeMap = {
94
+ cost: 'cost',
95
+ latency: 'latency',
96
+ throughput: 'throughput',
97
+ reliability: 'improvement',
98
+ };
99
+ // Parse impact percentage from the impact string (e.g., "90% savings" or "50% improvement")
100
+ const impactMatch = opt.impact.match(/(\d+)%/);
101
+ const impactPercent = impactMatch ? parseInt(impactMatch[1]) : 20;
102
+ insights.push({
103
+ severity,
104
+ category: categoryMap[opt.dimension] || opt.dimension,
105
+ headline: opt.description,
106
+ evidence: `${opt.type}: ${opt.impact}`,
107
+ location: `${opt.file}:${opt.line}`,
108
+ recommendation: opt.description,
109
+ impact: {
110
+ layer: opt.dimension,
111
+ impactType: impactTypeMap[opt.dimension] || 'improvement',
112
+ estimatedImpactPercent: impactPercent,
113
+ effort: opt.effort,
114
+ },
115
+ });
116
+ }
117
+ // Add reliability anti-pattern insights
118
+ for (const profile of analysis.performance_profiles) {
119
+ if (profile.reliability?.anti_patterns) {
120
+ for (const antiPattern of profile.reliability.anti_patterns) {
121
+ const severity = antiPattern.severity === 'high' ? 'critical' :
122
+ antiPattern.severity === 'medium' ? 'warning' : 'info';
123
+ insights.push({
124
+ severity,
125
+ category: 'Reliability Issue',
126
+ headline: antiPattern.pattern,
127
+ evidence: antiPattern.description,
128
+ location: antiPattern.location,
129
+ recommendation: `Fix: ${antiPattern.pattern}`,
130
+ impact: {
131
+ layer: 'reliability',
132
+ impactType: 'improvement',
133
+ estimatedImpactPercent: antiPattern.severity === 'high' ? 40 : 20,
134
+ effort: 'low',
135
+ },
136
+ });
137
+ }
138
+ }
139
+ }
140
+ return insights;
141
+ }
142
+ // =============================================================================
143
+ // PASS 1: PLAN
144
+ // =============================================================================
145
+ function detectMode(opts) {
146
+ // Check if the main path is an events file (case-insensitive for robustness)
147
+ const pathLower = opts.path.toLowerCase();
148
+ const isEventsFile = pathLower.endsWith('.jsonl') ||
149
+ pathLower.endsWith('.ndjson') ||
150
+ pathLower.endsWith('.json') ||
151
+ pathLower.endsWith('.csv');
152
+ // Also check if it's a file (not directory) - file paths with these extensions are events
153
+ const pathIsFile = !isDirectory(opts.path);
154
+ // Runtime mode: events file path without separate --events option
155
+ if (isEventsFile && pathIsFile && !opts.events) {
156
+ return 'runtime';
157
+ }
158
+ // Combined mode: directory path with --events option
159
+ if (!isEventsFile && opts.events) {
160
+ return 'combined';
161
+ }
162
+ // Combined mode: events file path with separate --events option (rare but valid)
163
+ if (isEventsFile && opts.events) {
164
+ return 'combined';
165
+ }
166
+ // Static mode: directory path without --events option
167
+ if (!isEventsFile && !opts.events) {
168
+ return 'static';
169
+ }
170
+ return 'combined';
171
+ }
172
+ function isDirectory(path) {
173
+ try {
174
+ return existsSync(path) && statSync(path).isDirectory();
175
+ }
176
+ catch {
177
+ return false;
178
+ }
179
+ }
180
+ /**
181
+ * Fetch runtime events from a URL
182
+ */
183
+ async function fetchEventsFromUrl(url) {
184
+ const response = await fetch(url);
185
+ if (!response.ok) {
186
+ throw new Error(`Failed to fetch events from ${url}: ${response.status} ${response.statusText}`);
187
+ }
188
+ return response.text();
189
+ }
190
+ export function plan(opts) {
191
+ const tasks = [];
192
+ let id = 1;
193
+ const mode = detectMode(opts);
194
+ const pathIsDirectory = isDirectory(opts.path);
195
+ // Generate run ID and check resumability
196
+ const inputs = {
197
+ repoRoot: isDirectory(opts.path) ? opts.path : undefined,
198
+ eventsPath: opts.events || (isDirectory(opts.path) ? undefined : opts.path),
199
+ offline: opts.offline,
200
+ };
201
+ const runId = generateRunId(inputs);
202
+ const resumeCheck = checkResumable(inputs);
203
+ const shouldResume = !opts.noCache && resumeCheck.canResume;
204
+ // If we can resume, skip analysis tasks
205
+ if (!shouldResume) {
206
+ // Always load pricing first
207
+ tasks.push({
208
+ id: id++,
209
+ type: 'scan', // Reusing for pricing load
210
+ description: 'Load pricing data',
211
+ });
212
+ // Only add static analysis tasks if path is a directory AND mode requires it
213
+ if ((mode === 'static' || mode === 'combined') && pathIsDirectory) {
214
+ tasks.push({
215
+ id: id++,
216
+ type: 'scan',
217
+ description: 'Scan repository',
218
+ });
219
+ // Unified analysis: discovery + profiling in single LLM call
220
+ tasks.push({
221
+ id: id++,
222
+ type: 'analyze',
223
+ description: 'Analyze and profile inference points',
224
+ depends_on: [id - 1],
225
+ });
226
+ }
227
+ if (mode === 'runtime' || mode === 'combined') {
228
+ tasks.push({
229
+ id: id++,
230
+ type: 'parse_events',
231
+ description: 'Parse runtime events',
232
+ });
233
+ // NEW: LLM-based runtime analysis for ALL modes with runtime data (Patterns v0.2)
234
+ tasks.push({
235
+ id: id++,
236
+ type: 'analyze', // Reuse 'analyze' type for runtime LLM analysis
237
+ description: 'Analyze runtime patterns',
238
+ depends_on: [id - 1],
239
+ });
240
+ }
241
+ if (mode === 'combined') {
242
+ tasks.push({
243
+ id: id++,
244
+ type: 'join',
245
+ description: 'Correlate static + runtime',
246
+ });
247
+ // NEW: LLM-based correlation analysis (Patterns v0.2)
248
+ tasks.push({
249
+ id: id++,
250
+ type: 'join', // Reuse 'join' type for correlation analysis
251
+ description: 'Analyze code-runtime drift',
252
+ depends_on: [id - 1],
253
+ });
254
+ }
255
+ tasks.push({
256
+ id: id++,
257
+ type: 'load_templates',
258
+ description: 'Load insight templates',
259
+ });
260
+ tasks.push({
261
+ id: id++,
262
+ type: 'generate_insights',
263
+ description: 'Generate findings',
264
+ });
265
+ // v1.5: Compare with previous run if requested
266
+ if (opts.compare) {
267
+ tasks.push({
268
+ id: id++,
269
+ type: 'compare',
270
+ description: 'Compare with previous run',
271
+ });
272
+ }
273
+ // v1.5: Generate deploy-time predictions if requested
274
+ if (opts.predict) {
275
+ tasks.push({
276
+ id: id++,
277
+ type: 'predict',
278
+ description: 'Generate latency predictions',
279
+ });
280
+ }
281
+ // v1.5: Always generate counterfactual insights (show optimization opportunities)
282
+ tasks.push({
283
+ id: id++,
284
+ type: 'counterfactuals',
285
+ description: 'Identify optimization opportunities',
286
+ });
287
+ if (opts.html) {
288
+ tasks.push({
289
+ id: id++,
290
+ type: 'generate_html',
291
+ description: 'Generate HTML report',
292
+ });
293
+ }
294
+ if (opts.pdf) {
295
+ tasks.push({
296
+ id: id++,
297
+ type: 'generate_pdf',
298
+ description: 'Generate PDF report',
299
+ });
300
+ }
301
+ tasks.push({
302
+ id: id++,
303
+ type: 'save_artifacts',
304
+ description: 'Save artifacts',
305
+ });
306
+ // v1.5: Save to history for comparison/prediction (unless --no-history)
307
+ if (!opts.noHistory) {
308
+ tasks.push({
309
+ id: id++,
310
+ type: 'save_history',
311
+ description: 'Save to history',
312
+ });
313
+ }
314
+ }
315
+ else {
316
+ // Resuming - just need to load cached artifacts
317
+ tasks.push({
318
+ id: id++,
319
+ type: 'scan', // Reusing for load cached
320
+ description: 'Load cached results',
321
+ });
322
+ // Always generate HTML if requested, even when resuming
323
+ if (opts.html) {
324
+ tasks.push({
325
+ id: id++,
326
+ type: 'generate_html',
327
+ description: 'Generate HTML report',
328
+ });
329
+ }
330
+ if (opts.pdf) {
331
+ tasks.push({
332
+ id: id++,
333
+ type: 'generate_pdf',
334
+ description: 'Generate PDF report',
335
+ });
336
+ }
337
+ if (opts.html || opts.pdf) {
338
+ tasks.push({
339
+ id: id++,
340
+ type: 'save_artifacts',
341
+ description: 'Save artifacts',
342
+ });
343
+ }
344
+ }
345
+ return {
346
+ plan: { mode, tasks },
347
+ runId,
348
+ canResume: shouldResume,
349
+ runDir: resumeCheck.runDir,
350
+ };
351
+ }
352
+ // =============================================================================
353
+ // PASS 2: EXECUTE
354
+ // =============================================================================
355
+ async function executeTask(task, ctx, templates, runDir, onProgress) {
356
+ switch (task.type) {
357
+ case 'scan':
358
+ if (task.description === 'Load pricing data') {
359
+ await loadPricing();
360
+ }
361
+ else if (task.description === 'Load cached results') {
362
+ // Resume from cache
363
+ if (runDir) {
364
+ const cached = loadArtifacts(runDir);
365
+ ctx.inferenceMap = cached.inferenceMap;
366
+ ctx.insights = cached.insights;
367
+ ctx.joined = cached.joined;
368
+ ctx.runtimeSummary = cached.runtime;
369
+ if (cached.inferenceMap) {
370
+ ctx.callsites = cached.inferenceMap.callsites;
371
+ }
372
+ if (cached.insights && cached.insights.length > 0) {
373
+ ctx.impactSummary = generateImpactSummary(cached.insights);
374
+ }
375
+ }
376
+ }
377
+ else {
378
+ // Validate that path is a directory before attempting to scan
379
+ if (!isDirectory(ctx.opts.path)) {
380
+ const ext = ctx.opts.path.toLowerCase();
381
+ if (ext.endsWith('.jsonl') || ext.endsWith('.ndjson') || ext.endsWith('.json') || ext.endsWith('.csv')) {
382
+ throw new Error(`Cannot scan file "${ctx.opts.path}" as a codebase. This looks like an events file - try 'peakinfer analyze ${ctx.opts.path}' for runtime analysis.`);
383
+ }
384
+ throw new Error(`Expected directory for static analysis, got file: ${ctx.opts.path}`);
385
+ }
386
+ // Agent SDK pattern: DiscoveryAgent with constrained tools (Glob/Grep/Read)
387
+ const discoveryResult = await DiscoveryAgent.execute({ root: ctx.opts.path });
388
+ ctx.scanResult = discoveryResult.result.scanResult;
389
+ const fileCount = ctx.scanResult?.files.length ?? 0;
390
+ onProgress?.({ phase: 'scanning', detail: `${fileCount} files` });
391
+ }
392
+ break;
393
+ case 'analyze':
394
+ // Handle static code analysis, runtime pattern analysis, AND performance profiling
395
+ if (task.description === 'Analyze runtime patterns') {
396
+ // NEW: LLM-based runtime analysis (Patterns v0.2)
397
+ if (!ctx.events || !ctx.runtimeSummary) {
398
+ ctx.warnings.push('Runtime analysis skipped: no events parsed');
399
+ break;
400
+ }
401
+ try {
402
+ // Get pricing context for models in the data
403
+ const models = Object.keys(ctx.runtimeSummary.byModel);
404
+ const pricingContext = getPricingContext(models);
405
+ // Emit progress: starting runtime pattern analysis
406
+ onProgress?.({ phase: 'analyzing', detail: `analyzing ${ctx.events.length} runtime events...` });
407
+ const runtimeResult = await RuntimeAnalyzerAgent.execute({
408
+ events: ctx.events,
409
+ runtimeSummary: ctx.runtimeSummary,
410
+ pricingContext,
411
+ });
412
+ // Store runtime insights for later merging
413
+ const runtimeInsights = runtimeResult.result.insights.map(i => ({
414
+ ...i,
415
+ source: 'llm',
416
+ }));
417
+ ctx.llmInsights = [...(ctx.llmInsights || []), ...runtimeInsights];
418
+ onProgress?.({ phase: 'analyzing', detail: `${runtimeResult.result.insights.length} runtime insights` });
419
+ }
420
+ catch (error) {
421
+ ctx.warnings.push(`Runtime analysis warning: ${error instanceof Error ? error.message : String(error)}`);
422
+ }
423
+ }
424
+ else if (task.description === 'Analyze and profile inference points') {
425
+ // UNIFIED: Discovery + Profiling in single LLM call
426
+ if (!ctx.scanResult)
427
+ throw new Error('Scan result required');
428
+ try {
429
+ const scanRoot = ctx.scanResult.root;
430
+ // Read all source files (no artificial limit - process all candidates)
431
+ const filesToAnalyze = ctx.scanResult.files
432
+ .map(f => {
433
+ const fullPath = resolve(scanRoot, f.path);
434
+ try {
435
+ return {
436
+ path: fullPath,
437
+ content: readFileSync(fullPath, 'utf-8'),
438
+ language: f.language,
439
+ };
440
+ }
441
+ catch {
442
+ return null;
443
+ }
444
+ })
445
+ .filter((f) => f !== null);
446
+ if (filesToAnalyze.length === 0) {
447
+ ctx.warnings.push('Analysis skipped: no source files available');
448
+ ctx.callsites = [];
449
+ ctx.llmInsights = [];
450
+ break;
451
+ }
452
+ // Run unified analysis (discovery + profiling in one LLM call)
453
+ // Pass progress callback for Claude Code-style per-file progress updates
454
+ const orchestrator = new StaticAnalysisOrchestrator();
455
+ ctx.staticAnalysis = await orchestrator.analyze({ files: filesToAnalyze }, (progressData) => {
456
+ // Forward progress to renderer with percent for progress bar
457
+ onProgress?.({
458
+ phase: 'analyzing',
459
+ percent: progressData.percent,
460
+ currentFile: progressData.currentFile,
461
+ detail: `${progressData.completed}/${progressData.total} files`,
462
+ });
463
+ });
464
+ // Extract callsites from unified analysis for rest of pipeline
465
+ const callsitesFromAnalysis = [];
466
+ for (const profile of ctx.staticAnalysis.performance_profiles) {
467
+ callsitesFromAnalysis.push({
468
+ id: profile.inference_point_id,
469
+ file: profile.file.replace(scanRoot + '/', '').replace(scanRoot, ''),
470
+ line: profile.line,
471
+ provider: profile.provider,
472
+ model: profile.model ?? null,
473
+ framework: null,
474
+ runtime: null,
475
+ patterns: {},
476
+ confidence: 0.9,
477
+ });
478
+ }
479
+ ctx.callsites = callsitesFromAnalysis;
480
+ // Convert performance profiles to insights
481
+ const performanceInsights = convertPerformanceToInsights(ctx.staticAnalysis);
482
+ ctx.llmInsights = performanceInsights;
483
+ // Build inference map
484
+ let promptMeta = { llmUsed: true };
485
+ try {
486
+ const prompt = getDefaultPrompt();
487
+ promptMeta.promptId = prompt.id;
488
+ promptMeta.promptVersion = prompt.version;
489
+ }
490
+ catch {
491
+ // Prompt not found, use defaults
492
+ }
493
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, ctx.callsites, promptMeta);
494
+ onProgress?.({
495
+ phase: 'profiling',
496
+ detail: `${ctx.staticAnalysis.summary.total_optimizations} optimizations found`,
497
+ });
498
+ }
499
+ catch (error) {
500
+ ctx.warnings.push(`Analysis warning: ${error instanceof Error ? error.message : String(error)}`);
501
+ ctx.callsites = [];
502
+ ctx.llmInsights = [];
503
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, [], { llmUsed: false });
504
+ }
505
+ }
506
+ else if (task.description === 'Profile performance') {
507
+ // Legacy: Skip - now handled by unified analysis above
508
+ break;
509
+ }
510
+ else {
511
+ // Original static code analysis
512
+ if (!ctx.scanResult)
513
+ throw new Error('Scan result required');
514
+ try {
515
+ // Agent SDK pattern: AnalyzerAgent with tool-limited semantic analysis
516
+ // Pass progress callback for visual progress bar during LLM analysis
517
+ const analyzerResult = await AnalyzerAgent.execute({
518
+ scanResult: ctx.scanResult,
519
+ onProgress: onProgress ? (data) => {
520
+ onProgress({ phase: 'analyzing', percent: data.percent, currentFile: data.currentFile });
521
+ } : undefined,
522
+ });
523
+ ctx.callsites = analyzerResult.result.callsites;
524
+ ctx.llmInsights = analyzerResult.result.llmInsights;
525
+ // Get prompt metadata for report
526
+ let promptMeta = { llmUsed: ctx.llmInsights.length > 0 };
527
+ try {
528
+ const prompt = getDefaultPrompt();
529
+ promptMeta.promptId = prompt.id;
530
+ promptMeta.promptVersion = prompt.version;
531
+ }
532
+ catch {
533
+ // Prompt not found, use defaults
534
+ }
535
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, ctx.callsites, promptMeta);
536
+ onProgress?.({ phase: 'analyzing', detail: `${ctx.callsites.length} inference points` });
537
+ }
538
+ catch (error) {
539
+ // Partial state: analysis failed but we can continue
540
+ ctx.warnings.push(`Analysis warning: ${error instanceof Error ? error.message : String(error)}`);
541
+ ctx.callsites = [];
542
+ ctx.llmInsights = [];
543
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, [], { llmUsed: false });
544
+ }
545
+ }
546
+ break;
547
+ case 'parse_events': {
548
+ try {
549
+ // Build normalization options from CLI flags (PRD §6.4)
550
+ const normalizationOptions = {
551
+ format_hint: ctx.opts.formatHint,
552
+ field_hints: ctx.opts.fieldHints,
553
+ lenient: ctx.opts.lenient,
554
+ strict: ctx.opts.strict,
555
+ codebase_context: ctx.scanResult, // Pass codebase context for smarter normalization
556
+ };
557
+ // Handle --events-url: fetch from URL first
558
+ if (ctx.opts.eventsUrl) {
559
+ const eventsContent = await fetchEventsFromUrl(ctx.opts.eventsUrl);
560
+ // Write to temp file for parsing
561
+ const tempPath = '.peakinfer/.tmp_events.jsonl';
562
+ writeFileSync(tempPath, eventsContent);
563
+ ctx.events = await parseEvents(tempPath, normalizationOptions);
564
+ }
565
+ else {
566
+ const eventsPath = ctx.opts.events || ctx.opts.path;
567
+ ctx.events = await parseEvents(eventsPath, normalizationOptions);
568
+ }
569
+ ctx.runtimeSummary = aggregate(ctx.events);
570
+ // Emit progress with event count
571
+ onProgress?.({ phase: 'parsing', detail: `${ctx.events.length} events` });
572
+ }
573
+ catch (error) {
574
+ // Partial state: event parsing failed
575
+ ctx.warnings.push(`Events parsing warning: ${error instanceof Error ? error.message : String(error)}`);
576
+ ctx.events = [];
577
+ }
578
+ break;
579
+ }
580
+ case 'join':
581
+ // Handle both basic join AND LLM-based correlation analysis
582
+ if (task.description === 'Analyze code-runtime drift') {
583
+ // NEW: LLM-based correlation analysis (Patterns v0.2)
584
+ if (!ctx.callsites || !ctx.events || !ctx.runtimeSummary) {
585
+ ctx.warnings.push('Correlation analysis skipped: missing callsites or events');
586
+ break;
587
+ }
588
+ try {
589
+ // Emit progress: starting drift detection analysis
590
+ onProgress?.({ phase: 'correlating', detail: 'detecting code-runtime drift...' });
591
+ const correlationResult = await CorrelationAnalyzerAgent.execute({
592
+ callsites: ctx.callsites,
593
+ events: ctx.events,
594
+ runtimeSummary: ctx.runtimeSummary,
595
+ });
596
+ // Merge correlation insights
597
+ const correlationInsights = correlationResult.result.insights.map(i => ({
598
+ ...i,
599
+ source: 'llm',
600
+ }));
601
+ ctx.llmInsights = [...(ctx.llmInsights || []), ...correlationInsights];
602
+ // Update drift signals in joined output
603
+ if (ctx.joined) {
604
+ ctx.joined.drift = [
605
+ ...ctx.joined.drift,
606
+ ...correlationResult.result.driftSignals,
607
+ ];
608
+ }
609
+ onProgress?.({
610
+ phase: 'correlating',
611
+ detail: `alignment: ${Math.round(correlationResult.result.alignmentScore * 100)}%`,
612
+ });
613
+ }
614
+ catch (error) {
615
+ ctx.warnings.push(`Correlation analysis warning: ${error instanceof Error ? error.message : String(error)}`);
616
+ }
617
+ }
618
+ else {
619
+ // Original basic join
620
+ if (!ctx.callsites || !ctx.events)
621
+ throw new Error('Callsites and events required');
622
+ // Agent SDK pattern: JoinerAgent correlates static + runtime
623
+ const joinerResult = await JoinerAgent.execute({ callsites: ctx.callsites, events: ctx.events });
624
+ ctx.joined = joinerResult.result.joined;
625
+ onProgress?.({
626
+ phase: 'correlating',
627
+ detail: `${ctx.joined.callsites.filter(c => 'usage' in c && c.usage).length} matched`,
628
+ });
629
+ }
630
+ break;
631
+ case 'load_templates':
632
+ // Templates already loaded, just verify
633
+ break;
634
+ case 'generate_insights': {
635
+ // Agent SDK pattern: InsightAgent evaluates templates
636
+ // For runtime-only mode, create synthetic inference points from events for template evaluation
637
+ let data;
638
+ if (ctx.joined) {
639
+ data = ctx.joined;
640
+ }
641
+ else if (ctx.callsites && ctx.callsites.length > 0) {
642
+ data = { callsites: ctx.callsites };
643
+ }
644
+ else if (ctx.events && ctx.events.length > 0) {
645
+ // Runtime-only mode: create synthetic enriched inference points from events
646
+ data = { callsites: createSyntheticCallsitesFromEvents(ctx.events) };
647
+ }
648
+ else {
649
+ data = { callsites: [] };
650
+ }
651
+ const insightResult = await InsightAgent.execute({ data, templates });
652
+ const templateInsights = insightResult.result.insights;
653
+ // Convert LLM insights to Insight format, preserving any LLM-provided impact estimates
654
+ const llmFormattedInsights = (ctx.llmInsights || []).map(llmInsight => ({
655
+ id: `llm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
656
+ severity: llmInsight.severity,
657
+ category: llmInsight.category,
658
+ headline: llmInsight.headline,
659
+ evidence: llmInsight.evidence,
660
+ location: llmInsight.location,
661
+ recommendation: llmInsight.recommendation,
662
+ source: 'llm', // Mark as LLM-generated
663
+ // Preserve LLM-provided impact estimates if present
664
+ impact: llmInsight.impact ? {
665
+ layer: llmInsight.impact.layer,
666
+ impactType: llmInsight.impact.impactType,
667
+ estimatedImpactPercent: llmInsight.impact.estimatedImpactPercent,
668
+ effort: llmInsight.impact.effort,
669
+ confidence: 0.8, // LLM estimates have higher confidence
670
+ } : undefined,
671
+ }));
672
+ // Combine: LLM semantic insights first (phase 1), then template pattern insights (phase 2)
673
+ const combinedInsights = [...llmFormattedInsights, ...templateInsights];
674
+ // Enrich all insights with impact estimates (fills in missing ones)
675
+ ctx.insights = enrichInsightsWithImpact(combinedInsights);
676
+ // Generate stack-ranked impact summary
677
+ ctx.impactSummary = generateImpactSummary(ctx.insights);
678
+ // Emit progress with insight count
679
+ onProgress?.({ phase: 'generating', detail: `${ctx.insights.length} findings` });
680
+ break;
681
+ }
682
+ case 'generate_html':
683
+ if (!ctx.inferenceMap)
684
+ throw new Error('InferenceMap required for HTML');
685
+ ctx.htmlContent = generateHTML({
686
+ inferenceMap: ctx.inferenceMap,
687
+ insights: ctx.insights || [],
688
+ joined: ctx.joined,
689
+ runtime: ctx.runtimeSummary,
690
+ });
691
+ break;
692
+ case 'generate_pdf': {
693
+ if (!ctx.htmlContent)
694
+ throw new Error('HTML content required for PDF');
695
+ // Generate human-friendly PDF filename
696
+ const pdfAbsolutePath = ctx.inferenceMap?.metadata?.absolutePath || ctx.opts.path;
697
+ const pdfProjectName = pdfAbsolutePath.split('/').filter(Boolean).pop() || 'project';
698
+ const pdfProjectSlug = pdfProjectName.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
699
+ const pdfFileName = `${pdfProjectSlug}_peakinfer_report.pdf`;
700
+ const pdfPath = `.peakinfer/${pdfFileName}`;
701
+ await generatePDF(ctx.htmlContent, pdfPath);
702
+ ctx.pdfPath = pdfPath;
703
+ break;
704
+ }
705
+ case 'save_artifacts': {
706
+ const inputs = {
707
+ repoRoot: isDirectory(ctx.opts.path) ? ctx.opts.path : undefined,
708
+ eventsPath: ctx.opts.events || (isDirectory(ctx.opts.path) ? undefined : ctx.opts.path),
709
+ offline: ctx.opts.offline,
710
+ };
711
+ // Extract project name for human-friendly report naming
712
+ const absolutePath = ctx.inferenceMap?.metadata?.absolutePath || ctx.opts.path;
713
+ const projectName = absolutePath.split('/').filter(Boolean).pop() || 'project';
714
+ saveArtifacts({
715
+ inferenceMap: ctx.inferenceMap,
716
+ insights: ctx.insights,
717
+ joined: ctx.joined,
718
+ runtime: ctx.runtimeSummary,
719
+ html: ctx.htmlContent,
720
+ }, '.peakinfer', {
721
+ runId: ctx.runId,
722
+ inputs,
723
+ projectName,
724
+ });
725
+ break;
726
+ }
727
+ case 'save_history': {
728
+ // v1.5: Save run to history for comparison/prediction features
729
+ const mode = ctx.joined ? 'combined' : (ctx.events?.length ? 'runtime' : 'static');
730
+ // Prepare analysis data for history storage
731
+ const historyData = {
732
+ inferenceMap: ctx.inferenceMap,
733
+ insights: ctx.insights,
734
+ joined: ctx.joined,
735
+ runtime: ctx.runtimeSummary,
736
+ };
737
+ // Generate human-friendly HTML path if generated
738
+ const absolutePath = ctx.inferenceMap?.metadata?.absolutePath || ctx.opts.path;
739
+ const projectName = absolutePath.split('/').filter(Boolean).pop() || 'project';
740
+ const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
741
+ saveRun({
742
+ path: ctx.opts.path,
743
+ analysisType: mode,
744
+ data: historyData,
745
+ htmlPath: ctx.htmlContent ? `.peakinfer/${projectSlug}_peakinfer_report.html` : undefined,
746
+ pdfPath: ctx.pdfPath,
747
+ });
748
+ break;
749
+ }
750
+ case 'compare': {
751
+ // v1.5: Compare with previous run
752
+ // Build current snapshot
753
+ const currentSnapshot = {
754
+ runId: ctx.runId,
755
+ timestamp: new Date().toISOString(),
756
+ callsites: ctx.callsites || [],
757
+ insights: ctx.insights,
758
+ };
759
+ // Get baseline (specific run or latest)
760
+ let baselineRun;
761
+ if (ctx.opts.compareRunId) {
762
+ baselineRun = loadRun(ctx.opts.compareRunId);
763
+ if (!baselineRun) {
764
+ ctx.warnings.push(`Comparison skipped: run ${ctx.opts.compareRunId} not found`);
765
+ break;
766
+ }
767
+ }
768
+ else {
769
+ baselineRun = getLatestRun(ctx.opts.path);
770
+ if (!baselineRun) {
771
+ ctx.warnings.push('Comparison skipped: no previous runs found');
772
+ break;
773
+ }
774
+ }
775
+ // Build baseline snapshot
776
+ const baselineSnapshot = {
777
+ runId: baselineRun.manifest.runId,
778
+ timestamp: baselineRun.manifest.timestamp,
779
+ callsites: baselineRun.data.inferenceMap?.callsites || [],
780
+ insights: baselineRun.data.insights,
781
+ };
782
+ // Perform comparison
783
+ ctx.comparison = compareSnapshots(baselineSnapshot, currentSnapshot);
784
+ // Log summary for progress
785
+ const summary = formatComparisonSummary(ctx.comparison);
786
+ onProgress?.({ phase: 'generating', detail: `compared with ${baselineRun.manifest.runId.slice(0, 8)}` });
787
+ break;
788
+ }
789
+ case 'predict': {
790
+ // v1.5: Generate deploy-time latency predictions
791
+ if (!ctx.inferenceMap) {
792
+ ctx.warnings.push('Prediction skipped: no inference map available');
793
+ break;
794
+ }
795
+ // Generate predictions based on inference points
796
+ const predictionResult = generatePredictions(ctx.inferenceMap, 0, // Historical run count (can be enhanced later with actual history)
797
+ { targetP95: ctx.opts.targetP95 });
798
+ ctx.prediction = predictionResult;
799
+ // Log summary for progress
800
+ const riskCount = predictionResult.summary.highRiskCount + predictionResult.summary.mediumRiskCount;
801
+ onProgress?.({
802
+ phase: 'generating',
803
+ detail: `${predictionResult.predictions.length} predictions, ${riskCount} at risk`,
804
+ });
805
+ break;
806
+ }
807
+ case 'counterfactuals': {
808
+ // v1.5: Generate what-if optimization scenarios
809
+ if (!ctx.inferenceMap) {
810
+ ctx.warnings.push('Counterfactuals skipped: no inference map available');
811
+ break;
812
+ }
813
+ // Generate counterfactual insights
814
+ const counterfactualResult = generateCounterfactuals(ctx.inferenceMap);
815
+ ctx.counterfactuals = counterfactualResult;
816
+ // Log summary for progress
817
+ onProgress?.({
818
+ phase: 'generating',
819
+ detail: `${counterfactualResult.summary.totalOpportunities} optimization opportunities`,
820
+ });
821
+ break;
822
+ }
823
+ }
824
+ }
825
+ function buildInferenceMap(root, callsites, metadata = {}) {
826
+ const providers = [...new Set(callsites.map(c => c.provider).filter(Boolean))];
827
+ const models = [...new Set(callsites.map(c => c.model).filter(Boolean))];
828
+ const patternCounts = {};
829
+ for (const cs of callsites) {
830
+ for (const [pattern, enabled] of Object.entries(cs.patterns)) {
831
+ if (enabled) {
832
+ patternCounts[pattern] = (patternCounts[pattern] || 0) + 1;
833
+ }
834
+ }
835
+ }
836
+ return {
837
+ version: '0.1', // InferenceMap schema version (not CLI version)
838
+ root,
839
+ generatedAt: new Date().toISOString(),
840
+ metadata: {
841
+ absolutePath: resolve(root),
842
+ promptId: metadata.promptId || 'peak-performance',
843
+ promptVersion: metadata.promptVersion,
844
+ templatesVersion: VERSION, // CLI version for audit trail
845
+ llmProvider: metadata.llmUsed ? 'anthropic' : 'none',
846
+ llmModel: metadata.llmUsed ? 'claude-sonnet-4-20250514' : undefined,
847
+ },
848
+ summary: {
849
+ totalCallsites: callsites.length,
850
+ providers,
851
+ models,
852
+ patterns: patternCounts,
853
+ },
854
+ callsites,
855
+ };
856
+ }
857
+ // =============================================================================
858
+ // PUBLIC API
859
+ // =============================================================================
860
+ export class Agent {
861
+ callbacks;
862
+ constructor(callbacks = {}) {
863
+ this.callbacks = callbacks;
864
+ }
865
+ async run(opts) {
866
+ const planResult = plan(opts);
867
+ const { plan: executionPlan, runId, canResume, runDir } = planResult;
868
+ // Notify if resuming from cache
869
+ if (canResume) {
870
+ this.callbacks.onResumed?.(runId);
871
+ }
872
+ this.callbacks.onPlanReady?.(executionPlan);
873
+ const ctx = {
874
+ opts,
875
+ runId,
876
+ resumed: canResume,
877
+ warnings: [],
878
+ };
879
+ const results = [];
880
+ // Load templates once (not needed if resuming)
881
+ const templates = canResume ? [] : await loadTemplates({ offline: opts.offline });
882
+ for (const task of executionPlan.tasks) {
883
+ this.callbacks.onTaskStart?.(task);
884
+ const startTime = Date.now();
885
+ try {
886
+ await executeTask(task, ctx, templates, runDir, this.callbacks.onProgress);
887
+ const result = {
888
+ taskId: task.id,
889
+ status: 'success',
890
+ durationMs: Date.now() - startTime,
891
+ };
892
+ results.push(result);
893
+ this.callbacks.onTaskComplete?.(task, result);
894
+ }
895
+ catch (error) {
896
+ const result = {
897
+ taskId: task.id,
898
+ status: 'failed',
899
+ error: error instanceof Error ? error.message : String(error),
900
+ durationMs: Date.now() - startTime,
901
+ };
902
+ results.push(result);
903
+ this.callbacks.onTaskComplete?.(task, result);
904
+ // Critical tasks abort execution only if not partial-safe
905
+ if (['scan'].includes(task.type) && task.description !== 'Load cached results') {
906
+ throw error;
907
+ }
908
+ // analyze and parse_events can fail gracefully (partial state)
909
+ }
910
+ }
911
+ // Notify if there were warnings (partial state)
912
+ if (ctx.warnings.length > 0) {
913
+ this.callbacks.onPartial?.(ctx.warnings);
914
+ }
915
+ // Generate human-friendly report filename
916
+ const absolutePath = ctx.inferenceMap?.metadata?.absolutePath || opts.path;
917
+ const projectName = absolutePath.split('/').filter(Boolean).pop() || 'project';
918
+ const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
919
+ const reportFileName = ctx.htmlContent
920
+ ? `.peakinfer/${projectSlug}_peakinfer_report.html`
921
+ : undefined;
922
+ const agentResults = {
923
+ mode: executionPlan.mode,
924
+ runId,
925
+ resumed: canResume,
926
+ scanResult: ctx.scanResult,
927
+ callsites: ctx.callsites,
928
+ events: ctx.events,
929
+ runtimeSummary: ctx.runtimeSummary,
930
+ joined: ctx.joined,
931
+ insights: ctx.insights || [],
932
+ impactSummary: ctx.impactSummary,
933
+ inferenceMap: ctx.inferenceMap,
934
+ staticAnalysis: ctx.staticAnalysis,
935
+ comparison: ctx.comparison, // v1.5: Historical comparison
936
+ prediction: ctx.prediction, // v1.5: Deploy-time predictions
937
+ counterfactuals: ctx.counterfactuals, // v1.5: What-if scenarios
938
+ htmlPath: reportFileName,
939
+ pdfPath: ctx.pdfPath,
940
+ warnings: ctx.warnings.length > 0 ? ctx.warnings : undefined,
941
+ };
942
+ // Handle --out: write output to file
943
+ if (opts.out) {
944
+ const outputData = {
945
+ schema: 'peakinfer-analysis',
946
+ version: '1.0', // Analysis export format version
947
+ cliVersion: VERSION,
948
+ mode: agentResults.mode,
949
+ runId: agentResults.runId,
950
+ timestamp: new Date().toISOString(),
951
+ inferenceMap: agentResults.inferenceMap,
952
+ insights: agentResults.insights,
953
+ impactSummary: agentResults.impactSummary,
954
+ comparison: agentResults.comparison,
955
+ prediction: agentResults.prediction,
956
+ counterfactuals: agentResults.counterfactuals,
957
+ runtimeSummary: agentResults.runtimeSummary,
958
+ };
959
+ writeFileSync(opts.out, JSON.stringify(outputData, null, 2));
960
+ }
961
+ this.callbacks.onComplete?.(agentResults);
962
+ return agentResults;
963
+ }
964
+ }
965
+ //# sourceMappingURL=agent.js.map