@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,1015 @@
1
+ /**
2
+ * Static Analysis Orchestrator
3
+ * Runs unified multi-dimensional analysis for comprehensive static code analysis
4
+ *
5
+ * Uses Claude Agent SDK (per TDD v1.9.3) with optimized single-call approach:
6
+ * one agent query per file analyzing all 4 dimensions (cost, latency, throughput, reliability)
7
+ *
8
+ * Architecture: Claude Agent SDK = Engine, TypeScript = Glue (per TDD §1)
9
+ *
10
+ * Prompts are loaded from YAML config files (prompts/*.yaml) for consistency
11
+ * between CLI and API surfaces.
12
+ *
13
+ * SYNC NOTE: This file is SYNCED FROM peakinfer-site (private repo).
14
+ * Source: peakinfer-site/lib/agents/static-orchestrator.ts
15
+ * DO NOT modify directly - changes must be made in peakinfer-site first.
16
+ */
17
+
18
+ import { query } from '@anthropic-ai/claude-agent-sdk';
19
+ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
20
+ import { EventEmitter } from 'events';
21
+
22
+ // Increase max listeners to handle parallel file analysis without warnings
23
+ // Each query() call adds exit listeners; with many files we exceed the default of 10
24
+ // Set to 500 to support large codebases (300+ files)
25
+ const originalMaxListeners = EventEmitter.defaultMaxListeners;
26
+ EventEmitter.defaultMaxListeners = 500;
27
+ process.setMaxListeners(500);
28
+
29
+ import type {
30
+ StaticAnalysisInput,
31
+ StaticAnalysisOutput,
32
+ PerformanceProfile,
33
+ ImportAnalyzerOutput,
34
+ CallSiteFinderOutput,
35
+ InferencePoint,
36
+ CostProfile,
37
+ LatencyProfile,
38
+ ThroughputProfile,
39
+ ReliabilityProfile,
40
+ Insight,
41
+ Issue,
42
+ } from './analysis-types.js';
43
+
44
+ // Re-export types for consumers
45
+ export type { StaticAnalysisInput, StaticAnalysisOutput, PerformanceProfile };
46
+ import { getUnifiedAnalyzerPrompt, formatUserMessage } from './prompts/loader.js';
47
+
48
+ // =============================================================================
49
+ // LOAD PROMPT FROM CONFIG
50
+ // =============================================================================
51
+
52
+ // Always load fresh from YAML config (no caching for serverless)
53
+ function getPrompt(): { system: string; userTemplate: string } {
54
+ return getUnifiedAnalyzerPrompt();
55
+ }
56
+
57
+ // =============================================================================
58
+ // HELPERS
59
+ // =============================================================================
60
+
61
+ /**
62
+ * Generate stable inference point ID using file:line format.
63
+ * This ensures IDs are consistent across runs for the same code location.
64
+ * Falls back to random ID only if no location info is available.
65
+ */
66
+ function generatePointId(filePath?: string, line?: number): string {
67
+ if (filePath && line) {
68
+ // Use stable file:line format (PRD requirement for consistent IDs)
69
+ return `${filePath}:${line}`;
70
+ }
71
+ // Fallback for rare cases where location is unknown
72
+ return 'pt_' + Math.random().toString(36).substring(2, 10);
73
+ }
74
+
75
+ /**
76
+ * Extract complete JSON object from text by matching brackets.
77
+ * This is more robust than regex which can match incomplete JSON.
78
+ * Returns null if the extracted JSON is too short or malformed.
79
+ *
80
+ * Handles common LLM issues:
81
+ * - Skips code snippets that aren't analysis results
82
+ * - Validates that response looks like analysis JSON (has "inference_points")
83
+ */
84
+ function extractJSON(text: string): string | null {
85
+ // Look for JSON that starts with {"inference_points" - the expected response format
86
+ // This avoids picking up code snippets that happen to start with {
87
+ const jsonStartPattern = /\{\s*"inference_points"/;
88
+ const match = text.match(jsonStartPattern);
89
+
90
+ if (!match || match.index === undefined) {
91
+ // Fallback: try to find any JSON object, but validate later
92
+ const fallbackStart = text.indexOf('{"');
93
+ if (fallbackStart === -1) return null;
94
+
95
+ const extracted = extractJSONFromPosition(text, fallbackStart);
96
+ if (extracted && isValidAnalysisJSON(extracted)) {
97
+ return extracted;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ return extractJSONFromPosition(text, match.index);
103
+ }
104
+
105
+ /**
106
+ * Extract JSON starting from a given position using bracket matching.
107
+ */
108
+ function extractJSONFromPosition(text: string, start: number): string | null {
109
+ let depth = 0;
110
+ let inString = false;
111
+ let escape = false;
112
+
113
+ for (let i = start; i < text.length; i++) {
114
+ const char = text[i];
115
+
116
+ if (escape) {
117
+ escape = false;
118
+ continue;
119
+ }
120
+
121
+ if (char === '\\' && inString) {
122
+ escape = true;
123
+ continue;
124
+ }
125
+
126
+ if (char === '"' && !escape) {
127
+ inString = !inString;
128
+ continue;
129
+ }
130
+
131
+ if (inString) continue;
132
+
133
+ if (char === '{') depth++;
134
+ if (char === '}') {
135
+ depth--;
136
+ if (depth === 0) {
137
+ const extracted = text.substring(start, i + 1);
138
+ // Sanity check: valid analysis JSON should be at least 50 chars
139
+ if (extracted.length < 50) {
140
+ return null;
141
+ }
142
+ return extracted;
143
+ }
144
+ }
145
+ }
146
+
147
+ return null;
148
+ }
149
+
150
+ /**
151
+ * Check if extracted text looks like valid analysis JSON.
152
+ * Rejects code snippets and placeholder responses.
153
+ */
154
+ function isValidAnalysisJSON(text: string): boolean {
155
+ // Reject if it contains placeholder syntax like [...] or {...}
156
+ if (/\[\.\.\.\]|\{\.\.\.\}/.test(text)) {
157
+ return false;
158
+ }
159
+
160
+ // Reject if it looks like code (unquoted keys with single quotes or variable names)
161
+ // Valid JSON has "key": not key: or 'key':
162
+ if (/[{,]\s*[a-zA-Z_][a-zA-Z0-9_]*\s*:/.test(text)) {
163
+ // Check if this is actually unquoted keys (not inside a string)
164
+ // Simple heuristic: if we see word: followed by non-string value, it's likely code
165
+ if (/[{,]\s*[a-zA-Z_]\w*\s*:\s*[a-zA-Z_]/.test(text)) {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ // Must contain "inference_points" for it to be a valid response
171
+ if (!text.includes('"inference_points"')) {
172
+ return false;
173
+ }
174
+
175
+ return true;
176
+ }
177
+
178
+ function detectLanguage(filePath: string): string {
179
+ const ext = filePath.split('.').pop()?.toLowerCase() || '';
180
+ const languageMap: Record<string, string> = {
181
+ 'py': 'python',
182
+ 'js': 'javascript',
183
+ 'ts': 'typescript',
184
+ 'tsx': 'typescript',
185
+ 'jsx': 'javascript',
186
+ 'go': 'go',
187
+ 'rs': 'rust',
188
+ 'java': 'java',
189
+ 'rb': 'ruby',
190
+ 'php': 'php',
191
+ };
192
+ return languageMap[ext] || 'unknown';
193
+ }
194
+
195
+ // =============================================================================
196
+ // UNIFIED ANALYSIS RESULT TYPES
197
+ // =============================================================================
198
+
199
+ interface UnifiedIssue {
200
+ type: string;
201
+ severity: string;
202
+ headline: string;
203
+ evidence: string;
204
+ original_code: string;
205
+ suggested_fix: string | null;
206
+ ai_agent_prompt: string;
207
+ }
208
+
209
+ interface UnifiedAnalysisResult {
210
+ inference_points: Array<{
211
+ id: string;
212
+ line: number;
213
+ provider: string;
214
+ model: string | null;
215
+ call_type: string;
216
+ original_code?: string;
217
+ cost_profile: {
218
+ tier: string;
219
+ estimated_cost_per_call: number;
220
+ optimizations: Array<{ type: string; description: string; savings_percent: number }>;
221
+ };
222
+ latency_profile: {
223
+ estimated_p95_ms: number;
224
+ is_blocking: boolean;
225
+ has_streaming: boolean;
226
+ optimizations: Array<{ type: string; description: string; improvement_percent: number }>;
227
+ };
228
+ throughput_profile: {
229
+ has_rate_limiting: boolean;
230
+ has_batching: boolean;
231
+ bottlenecks: Array<{ type: string; description: string }>;
232
+ optimizations: Array<{ type: string; description: string; improvement: string }>;
233
+ };
234
+ reliability_profile: {
235
+ has_error_handling: boolean;
236
+ has_retry: boolean;
237
+ has_timeout: boolean;
238
+ has_fallback: boolean;
239
+ anti_patterns: Array<{ type: string; description: string }>;
240
+ optimizations: Array<{ type: string; description: string; priority: string }>;
241
+ };
242
+ issues?: UnifiedIssue[];
243
+ }>;
244
+ imports: {
245
+ llm_providers: string[];
246
+ frameworks: string[];
247
+ };
248
+ insights?: Array<{
249
+ severity: string;
250
+ category: string;
251
+ headline: string;
252
+ evidence: string;
253
+ recommendation?: string;
254
+ }>;
255
+ }
256
+
257
+ // =============================================================================
258
+ // UNIFIED SINGLE-CALL ANALYSIS (Claude Agent SDK)
259
+ // =============================================================================
260
+
261
+ /**
262
+ * Extract text content from Claude Agent SDK messages
263
+ */
264
+ function extractTextFromMessages(messages: SDKMessage[]): string {
265
+ let text = '';
266
+ for (const msg of messages) {
267
+ if (msg.type === 'assistant' && msg.message?.content) {
268
+ for (const block of msg.message.content) {
269
+ if (block.type === 'text') {
270
+ text += block.text;
271
+ }
272
+ }
273
+ }
274
+ }
275
+ return text;
276
+ }
277
+
278
+ async function runUnifiedAnalysis(
279
+ filePath: string,
280
+ content: string,
281
+ language: string,
282
+ _anthropicKey: string // Kept for signature compatibility, SDK uses env var
283
+ ): Promise<UnifiedAnalysisResult | null> {
284
+ // Load prompt from config file
285
+ const configPrompt = getPrompt();
286
+
287
+ // Format user message using template from config
288
+ const userMessage = configPrompt.userTemplate
289
+ ? formatUserMessage(configPrompt.userTemplate, {
290
+ language,
291
+ file_path: filePath,
292
+ content,
293
+ })
294
+ : `Analyze this ${language} file for LLM inference points and their performance characteristics:
295
+
296
+ File: ${filePath}
297
+
298
+ \`\`\`${language}
299
+ ${content}
300
+ \`\`\`
301
+
302
+ Find all LLM API calls (Anthropic Claude, Claude Agent SDK, self-hosted like TensorRT-LLM, vLLM, Triton, HTTP calls to inference endpoints, etc.) and analyze each one.`;
303
+
304
+ try {
305
+ // Use Claude Agent SDK query() function (per TDD v1.9.3)
306
+ const agentQuery = query({
307
+ prompt: userMessage,
308
+ options: {
309
+ systemPrompt: configPrompt.system,
310
+ model: 'claude-sonnet-4-20250514',
311
+ // Disable tools - we just want analysis output
312
+ tools: [],
313
+ // Use plan mode to avoid file operations
314
+ permissionMode: 'plan',
315
+ // Set working directory
316
+ cwd: process.cwd(),
317
+ },
318
+ });
319
+
320
+ // Collect all messages from the async generator
321
+ const messages: SDKMessage[] = [];
322
+ for await (const message of agentQuery) {
323
+ messages.push(message);
324
+ }
325
+
326
+ // Extract text content from messages
327
+ const responseText = extractTextFromMessages(messages);
328
+
329
+ if (responseText) {
330
+ // Use robust JSON extraction instead of greedy regex
331
+ const jsonStr = extractJSON(responseText);
332
+ if (jsonStr) {
333
+ try {
334
+ const parsed = JSON.parse(jsonStr);
335
+ // Validate that we got a proper response structure
336
+ if (!parsed || typeof parsed !== 'object') {
337
+ // Silent: file probably doesn't have LLM calls
338
+ return null;
339
+ }
340
+ // Ensure inference_points array exists (even if empty)
341
+ if (!parsed.inference_points) {
342
+ parsed.inference_points = [];
343
+ }
344
+ // Ensure imports object exists
345
+ if (!parsed.imports) {
346
+ parsed.imports = { llm_providers: [], frameworks: [] };
347
+ }
348
+ // Ensure IDs are set using stable file:line format
349
+ for (const point of parsed.inference_points) {
350
+ if (!point.id) {
351
+ point.id = generatePointId(filePath, point.line);
352
+ }
353
+ }
354
+ return parsed as UnifiedAnalysisResult;
355
+ } catch (_parseError) {
356
+ // JSON extraction found something but it wasn't valid JSON
357
+ // This is expected for files without LLM calls - LLM may return code snippets
358
+ // Only log in verbose mode or if debugging is needed
359
+ return null;
360
+ }
361
+ }
362
+ // No valid JSON found - this is normal for files without LLM inference points
363
+ }
364
+ } catch (error) {
365
+ // Only log actual SDK errors, not expected "no inference points" cases
366
+ const errorMsg = error instanceof Error ? error.message : String(error);
367
+ if (!errorMsg.includes('rate limit') && !errorMsg.includes('timeout')) {
368
+ // Silent for most errors - files without LLM calls are expected
369
+ } else {
370
+ console.error('Claude Agent SDK error:', errorMsg);
371
+ }
372
+ }
373
+
374
+ return null;
375
+ }
376
+
377
+ // =============================================================================
378
+ // CONVERT UNIFIED TO LEGACY FORMAT
379
+ // =============================================================================
380
+
381
+ function convertUnifiedToLegacy(
382
+ unified: UnifiedAnalysisResult,
383
+ filePath: string
384
+ ): {
385
+ imports: ImportAnalyzerOutput;
386
+ callsites: CallSiteFinderOutput;
387
+ costProfiles: CostProfile[];
388
+ latencyProfiles: LatencyProfile[];
389
+ throughputProfiles: ThroughputProfile[];
390
+ reliabilityProfiles: ReliabilityProfile[];
391
+ insights: Insight[];
392
+ } {
393
+ const providers = unified.imports?.llm_providers || [];
394
+ const frameworks = unified.imports?.frameworks || [];
395
+
396
+ const imports: ImportAnalyzerOutput = {
397
+ sdks: providers.map((p, i) => ({
398
+ name: p,
399
+ provider: p,
400
+ import_line: i + 1,
401
+ alias: null,
402
+ confidence: 0.9,
403
+ })),
404
+ frameworks: frameworks.map((f, i) => ({
405
+ name: f,
406
+ import_line: i + 1,
407
+ components: [],
408
+ confidence: 0.9,
409
+ })),
410
+ custom_wrappers: [],
411
+ infrastructure: [],
412
+ summary: {
413
+ has_llm_usage: providers.length > 0 || frameworks.length > 0,
414
+ primary_provider: providers[0] || null,
415
+ framework: frameworks[0] || null,
416
+ complexity: providers.length > 1 ? 'complex' : providers.length > 0 ? 'moderate' : 'simple',
417
+ },
418
+ };
419
+
420
+ const inferencePoints: InferencePoint[] = unified.inference_points.map(p => ({
421
+ id: p.id,
422
+ line: p.line,
423
+ column: 0,
424
+ function_context: '',
425
+ class_context: null,
426
+ call_expression: '',
427
+ call_type: (p.call_type || 'direct') as 'direct' | 'wrapper' | 'framework' | 'http',
428
+ provider: { value: p.provider, source: 'hardcoded' as const, confidence: 0.9 },
429
+ model: { value: p.model, source: 'hardcoded' as const, confidence: 0.8 },
430
+ is_async: false,
431
+ in_loop: false,
432
+ loop_type: 'none' as const,
433
+ estimated_calls: 'single' as const,
434
+ needs_tracing: false,
435
+ confidence: 0.85,
436
+ }));
437
+
438
+ const callsites: CallSiteFinderOutput = {
439
+ inference_points: inferencePoints,
440
+ wrapper_definitions: [],
441
+ summary: {
442
+ total_inference_points: inferencePoints.length,
443
+ direct_calls: inferencePoints.filter(p => p.call_type === 'direct').length,
444
+ wrapped_calls: inferencePoints.filter(p => p.call_type === 'wrapper').length,
445
+ framework_calls: inferencePoints.filter(p => p.call_type === 'framework').length,
446
+ providers_detected: [...new Set(inferencePoints.map(p => p.provider.value))],
447
+ models_detected: [...new Set(inferencePoints.map(p => p.model.value).filter(Boolean))] as string[],
448
+ has_dynamic_routing: false,
449
+ },
450
+ };
451
+
452
+ const costProfiles: CostProfile[] = unified.inference_points.map(p => ({
453
+ inference_point_id: p.id,
454
+ line: p.line,
455
+ model_analysis: {
456
+ model: p.model || 'unknown',
457
+ tier: (p.cost_profile?.tier || 'unknown') as 'premium' | 'standard' | 'budget' | 'unknown',
458
+ pricing: { input_per_1m: 0, output_per_1m: 0 },
459
+ is_overqualified: false,
460
+ reason: null,
461
+ },
462
+ token_estimates: {
463
+ input: { min: 100, typical: 500, max: 2000, basis: 'estimate' },
464
+ output: { min: 50, typical: 200, max: 1000, basis: 'estimate' },
465
+ has_few_shot: false,
466
+ few_shot_tokens: 0,
467
+ has_rag_context: false,
468
+ rag_context_estimate: 0,
469
+ },
470
+ call_frequency: { pattern: 'single' as const, multiplier: 1, loop_bound: 'bounded' as const, estimated_calls_per_invocation: 1 },
471
+ cost_estimate: {
472
+ per_call_min: p.cost_profile?.estimated_cost_per_call || 0,
473
+ per_call_typical: p.cost_profile?.estimated_cost_per_call || 0,
474
+ per_call_max: p.cost_profile?.estimated_cost_per_call || 0,
475
+ currency: 'USD',
476
+ },
477
+ cost_risk: { level: 'low', factors: [], unbounded_growth: false, context_accumulation: false },
478
+ optimizations: (p.cost_profile?.optimizations || []).map(o => ({
479
+ type: o.type as CostProfile['optimizations'][0]['type'],
480
+ description: o.description,
481
+ current_cost: '',
482
+ optimized_cost: '',
483
+ savings_percent: o.savings_percent,
484
+ effort: 'medium' as const,
485
+ sample_change: null,
486
+ })),
487
+ confidence: 0.85,
488
+ }));
489
+
490
+ const latencyProfiles: LatencyProfile[] = unified.inference_points.map(p => ({
491
+ inference_point_id: p.id,
492
+ line: p.line,
493
+ blocking_analysis: {
494
+ is_blocking: p.latency_profile?.is_blocking || false,
495
+ is_in_request_handler: false,
496
+ blocks_event_loop: p.latency_profile?.is_blocking || false,
497
+ handler_type: 'unknown' as const,
498
+ user_facing: false,
499
+ },
500
+ streaming_analysis: {
501
+ streaming_enabled: p.latency_profile?.has_streaming || false,
502
+ should_enable_streaming: !p.latency_profile?.has_streaming,
503
+ reason: p.latency_profile?.has_streaming ? 'Streaming already enabled' : 'Enable streaming for better UX',
504
+ time_to_first_token_benefit: !p.latency_profile?.has_streaming ? '~200ms vs full wait' : null,
505
+ },
506
+ async_analysis: {
507
+ is_async: false,
508
+ uses_await: false,
509
+ could_be_async: true,
510
+ async_benefit: 'Enable concurrency',
511
+ },
512
+ parallel_analysis: {
513
+ has_parallel_potential: false,
514
+ independent_calls: 0,
515
+ current_pattern: 'sequential' as const,
516
+ parallelizable_calls: [],
517
+ parallel_speedup_estimate: null,
518
+ },
519
+ chain_analysis: {
520
+ chain_depth: 1,
521
+ sequential_calls: 1,
522
+ total_latency_estimate: { min_ms: 500, typical_ms: 2000, max_ms: 5000 },
523
+ chain_pattern: 'single' as const,
524
+ },
525
+ timeout_analysis: {
526
+ timeout_configured: p.reliability_profile?.has_timeout || false,
527
+ timeout_value_ms: null,
528
+ has_fallback_on_timeout: false,
529
+ timeout_risk: p.reliability_profile?.has_timeout ? 'low' : 'high',
530
+ },
531
+ latency_risk: { level: 'medium', factors: [], tail_latency_risk: true, unpredictable: false },
532
+ latency_estimate: {
533
+ min_ms: 500,
534
+ typical_ms: 2000,
535
+ p95_ms: p.latency_profile?.estimated_p95_ms || 5000,
536
+ max_ms: 15000,
537
+ basis: 'Model estimate',
538
+ },
539
+ optimizations: (p.latency_profile?.optimizations || []).map(o => ({
540
+ type: o.type as LatencyProfile['optimizations'][0]['type'],
541
+ description: o.description,
542
+ current_latency: '',
543
+ optimized_latency: '',
544
+ improvement_percent: o.improvement_percent,
545
+ effort: 'medium' as const,
546
+ sample_change: null,
547
+ })),
548
+ confidence: 0.85,
549
+ }));
550
+
551
+ const throughputProfiles: ThroughputProfile[] = unified.inference_points.map(p => ({
552
+ inference_point_id: p.id,
553
+ line: p.line,
554
+ concurrency_analysis: {
555
+ concurrency_limit: null,
556
+ limit_source: 'none' as const,
557
+ limit_location: null,
558
+ is_global_limit: false,
559
+ recommended_limit: 10,
560
+ },
561
+ rate_limiting: {
562
+ has_rate_limiter: p.throughput_profile?.has_rate_limiting || false,
563
+ rate_limit_type: 'none' as const,
564
+ requests_per_minute: null,
565
+ handles_429: false,
566
+ backoff_strategy: 'none' as const,
567
+ },
568
+ batching_analysis: {
569
+ batching_enabled: p.throughput_profile?.has_batching || false,
570
+ batch_size: null,
571
+ could_batch: !p.throughput_profile?.has_batching,
572
+ batching_benefit: 'Reduce API calls',
573
+ batch_api_available: true,
574
+ },
575
+ queue_analysis: { uses_queue: false, queue_type: 'none' as const, async_processing: false, worker_pattern: false },
576
+ scaling_analysis: {
577
+ horizontally_scalable: true,
578
+ bottlenecks: (p.throughput_profile?.bottlenecks || []).map(b => ({
579
+ type: 'shared_state' as const,
580
+ location: filePath,
581
+ description: b.description,
582
+ severity: 'medium' as const,
583
+ })),
584
+ stateless: true,
585
+ client_reuse: false,
586
+ },
587
+ capacity_estimate: { max_concurrent_calls: 10, estimated_rps: 10, limiting_factor: 'API quota' },
588
+ throughput_risk: {
589
+ level: 'medium',
590
+ factors: [],
591
+ will_hit_rate_limits: !p.throughput_profile?.has_rate_limiting,
592
+ scaling_blocked: false,
593
+ },
594
+ optimizations: (p.throughput_profile?.optimizations || []).map(o => ({
595
+ type: o.type as ThroughputProfile['optimizations'][0]['type'],
596
+ description: o.description,
597
+ current_throughput: '',
598
+ optimized_throughput: '',
599
+ improvement: o.improvement,
600
+ effort: 'medium' as const,
601
+ sample_change: null,
602
+ })),
603
+ confidence: 0.85,
604
+ }));
605
+
606
+ const reliabilityProfiles: ReliabilityProfile[] = unified.inference_points.map(p => ({
607
+ inference_point_id: p.id,
608
+ line: p.line,
609
+ error_handling: {
610
+ has_try_catch: p.reliability_profile?.has_error_handling || false,
611
+ caught_exceptions: [],
612
+ specific_llm_errors: false,
613
+ error_logged: false,
614
+ error_propagated: false,
615
+ silent_failure: false,
616
+ user_friendly_error: false,
617
+ },
618
+ retry_strategy: {
619
+ has_retry: p.reliability_profile?.has_retry || false,
620
+ retry_library: 'none' as const,
621
+ max_retries: null,
622
+ backoff_type: 'none' as const,
623
+ initial_delay_ms: null,
624
+ max_delay_ms: null,
625
+ retry_on: [],
626
+ jitter: false,
627
+ retry_budget_risk: 'none' as const,
628
+ },
629
+ fallback_strategy: {
630
+ has_fallback: p.reliability_profile?.has_fallback || false,
631
+ fallback_type: 'none' as const,
632
+ fallback_model: null,
633
+ fallback_provider: null,
634
+ graceful_degradation: false,
635
+ fallback_tested: 'unknown' as const,
636
+ },
637
+ timeout_handling: {
638
+ timeout_configured: p.reliability_profile?.has_timeout || false,
639
+ timeout_ms: null,
640
+ timeout_source: 'none' as const,
641
+ on_timeout: 'none' as const,
642
+ },
643
+ circuit_breaker: {
644
+ has_circuit_breaker: false,
645
+ library: null,
646
+ failure_threshold: null,
647
+ recovery_time_ms: null,
648
+ },
649
+ validation: {
650
+ validates_response: false,
651
+ validates_json: false,
652
+ validates_schema: false,
653
+ handles_empty_response: false,
654
+ handles_truncated: false,
655
+ },
656
+ reliability_risk: {
657
+ level: p.reliability_profile?.has_error_handling ? 'moderate' : 'fragile',
658
+ factors: [],
659
+ single_point_of_failure: !p.reliability_profile?.has_fallback,
660
+ cascade_risk: false,
661
+ data_loss_risk: false,
662
+ },
663
+ anti_patterns: (p.reliability_profile?.anti_patterns || []).map(a => ({
664
+ pattern: a.type,
665
+ description: a.description,
666
+ location: filePath,
667
+ severity: 'medium' as const,
668
+ })),
669
+ optimizations: (p.reliability_profile?.optimizations || []).map(o => ({
670
+ type: o.type as ReliabilityProfile['optimizations'][0]['type'],
671
+ description: o.description,
672
+ reliability_before: 'low',
673
+ reliability_after: 'high',
674
+ effort: 'medium' as const,
675
+ priority: (o.priority || 'medium') as 'high' | 'medium' | 'low' | 'critical',
676
+ sample_change: null,
677
+ })),
678
+ confidence: 0.85,
679
+ }));
680
+
681
+ // Convert insights
682
+ const insights: Insight[] = (unified.insights || []).map((i, idx) => ({
683
+ id: `insight_${Date.now()}_${idx}`,
684
+ severity: i.severity as 'critical' | 'warning' | 'info',
685
+ category: i.category as Insight['category'],
686
+ headline: i.headline,
687
+ evidence: i.evidence,
688
+ location: filePath,
689
+ recommendation: i.recommendation,
690
+ source: 'llm' as const,
691
+ }));
692
+
693
+ return { imports, callsites, costProfiles, latencyProfiles, throughputProfiles, reliabilityProfiles, insights };
694
+ }
695
+
696
+ // =============================================================================
697
+ // ORCHESTRATOR CLASS
698
+ // =============================================================================
699
+
700
+ // Progress callback for Claude Code-style TUI feedback
701
+ export interface AnalysisProgressCallback {
702
+ (data: {
703
+ phase: 'analyzing';
704
+ completed: number;
705
+ total: number;
706
+ currentFile?: string;
707
+ percent: number;
708
+ }): void;
709
+ }
710
+
711
+ export class StaticAnalysisOrchestrator {
712
+ private anthropicKey: string;
713
+
714
+ constructor(anthropicKey?: string) {
715
+ // BYOK mode: user provides their own API key
716
+ this.anthropicKey = anthropicKey || process.env.ANTHROPIC_API_KEY || '';
717
+ if (!this.anthropicKey) {
718
+ throw new Error('ANTHROPIC_API_KEY is required. Set it via environment variable or pass it to the constructor.');
719
+ }
720
+ }
721
+
722
+ async analyze(
723
+ input: StaticAnalysisInput,
724
+ onProgress?: AnalysisProgressCallback
725
+ ): Promise<StaticAnalysisOutput> {
726
+ const allImports: ImportAnalyzerOutput[] = [];
727
+ const allCallsites: CallSiteFinderOutput[] = [];
728
+ const allCostAnalysis: StaticAnalysisOutput['cost_analysis'] = [];
729
+ const allLatencyAnalysis: StaticAnalysisOutput['latency_analysis'] = [];
730
+ const allThroughputAnalysis: StaticAnalysisOutput['throughput_analysis'] = [];
731
+ const allReliabilityAnalysis: StaticAnalysisOutput['reliability_analysis'] = [];
732
+ const allPerformanceProfiles: PerformanceProfile[] = [];
733
+ const allInsights: Insight[] = [];
734
+
735
+ // Track progress for Claude Code-style TUI
736
+ const totalFiles = input.files.length;
737
+ let completedFiles = 0;
738
+
739
+ // Emit initial progress
740
+ onProgress?.({
741
+ phase: 'analyzing',
742
+ completed: 0,
743
+ total: totalFiles,
744
+ percent: 0,
745
+ });
746
+
747
+ // Concurrent analysis with limited parallelism to avoid overwhelming system
748
+ // Max 20 concurrent API calls - balances speed vs resource usage
749
+ const MAX_CONCURRENT = 20;
750
+ const fileAnalyses: ({ file: typeof input.files[0]; unified: UnifiedAnalysisResult; language: string } | null)[] = [];
751
+
752
+ // Process files in batches
753
+ for (let i = 0; i < input.files.length; i += MAX_CONCURRENT) {
754
+ const batch = input.files.slice(i, i + MAX_CONCURRENT);
755
+ const batchResults = await Promise.all(
756
+ batch.map(async (file) => {
757
+ const language = file.language || detectLanguage(file.path);
758
+ const unified = await runUnifiedAnalysis(file.path, file.content, language, this.anthropicKey);
759
+
760
+ // Emit progress after each file completes (Claude Code pattern)
761
+ completedFiles++;
762
+ const percent = Math.round((completedFiles / totalFiles) * 100);
763
+ onProgress?.({
764
+ phase: 'analyzing',
765
+ completed: completedFiles,
766
+ total: totalFiles,
767
+ currentFile: file.path.split('/').pop() || file.path,
768
+ percent,
769
+ });
770
+
771
+ if (unified && unified.inference_points?.length > 0) {
772
+ return { file, unified, language };
773
+ }
774
+ return null;
775
+ })
776
+ );
777
+ fileAnalyses.push(...batchResults);
778
+ }
779
+
780
+ // Process results
781
+ for (const result of fileAnalyses) {
782
+ if (!result) continue;
783
+
784
+ const { file, unified } = result;
785
+ const converted = convertUnifiedToLegacy(unified, file.path);
786
+
787
+ allImports.push(converted.imports);
788
+ allCallsites.push(converted.callsites);
789
+ allInsights.push(...converted.insights);
790
+
791
+ allCostAnalysis.push({
792
+ cost_profiles: converted.costProfiles,
793
+ summary: {
794
+ total_inference_points: converted.costProfiles.length,
795
+ estimated_cost_per_1k_calls: converted.costProfiles.reduce((sum, p) => sum + (p.cost_estimate?.per_call_typical || 0) * 1000, 0),
796
+ highest_cost_point: null,
797
+ optimization_potential_percent: 50,
798
+ },
799
+ });
800
+
801
+ allLatencyAnalysis.push({
802
+ latency_profiles: converted.latencyProfiles,
803
+ summary: {
804
+ total_inference_points: converted.latencyProfiles.length,
805
+ blocking_calls: converted.latencyProfiles.filter(p => p.blocking_analysis?.is_blocking).length,
806
+ streaming_enabled: converted.latencyProfiles.filter(p => p.streaming_analysis?.streaming_enabled).length,
807
+ parallelizable: 0,
808
+ estimated_p95_ms: Math.max(...converted.latencyProfiles.map(p => p.latency_estimate?.p95_ms || 0), 5000),
809
+ },
810
+ });
811
+
812
+ allThroughputAnalysis.push({
813
+ throughput_profiles: converted.throughputProfiles,
814
+ summary: {
815
+ total_inference_points: converted.throughputProfiles.length,
816
+ has_rate_limiting: converted.throughputProfiles.filter(p => p.rate_limiting?.has_rate_limiter).length,
817
+ has_batching: converted.throughputProfiles.filter(p => p.batching_analysis?.batching_enabled).length,
818
+ scaling_bottlenecks: converted.throughputProfiles.reduce((sum, p) => sum + (p.scaling_analysis?.bottlenecks?.length || 0), 0),
819
+ estimated_max_rps: null,
820
+ },
821
+ });
822
+
823
+ allReliabilityAnalysis.push({
824
+ reliability_profiles: converted.reliabilityProfiles,
825
+ summary: {
826
+ total_inference_points: converted.reliabilityProfiles.length,
827
+ has_error_handling: converted.reliabilityProfiles.filter(p => p.error_handling?.has_try_catch).length,
828
+ has_retry: converted.reliabilityProfiles.filter(p => p.retry_strategy?.has_retry).length,
829
+ has_fallback: converted.reliabilityProfiles.filter(p => p.fallback_strategy?.has_fallback).length,
830
+ anti_patterns_found: converted.reliabilityProfiles.reduce((sum, p) => sum + (p.anti_patterns?.length || 0), 0),
831
+ overall_reliability: 'moderate',
832
+ },
833
+ });
834
+
835
+ // Build performance profiles with issues from LLM
836
+ for (const point of converted.callsites.inference_points) {
837
+ // Find matching unified inference point to get issues
838
+ const unifiedPoint = unified.inference_points.find(up => up.id === point.id);
839
+
840
+ // Convert LLM-generated issues to Issue type
841
+ const issues: Issue[] = (unifiedPoint?.issues || []).map(issue => ({
842
+ type: issue.type,
843
+ severity: (issue.severity || 'warning') as 'critical' | 'warning' | 'info',
844
+ headline: issue.headline,
845
+ evidence: issue.evidence,
846
+ originalCode: issue.original_code || '',
847
+ suggestedFix: issue.suggested_fix || null,
848
+ aiAgentPrompt: issue.ai_agent_prompt || '',
849
+ }));
850
+
851
+ allPerformanceProfiles.push({
852
+ inference_point_id: point.id,
853
+ line: point.line,
854
+ file: file.path,
855
+ provider: point.provider.value,
856
+ model: point.model.value,
857
+ originalCode: unifiedPoint?.original_code || '',
858
+ issues,
859
+ cost: converted.costProfiles.find(p => p.inference_point_id === point.id) || null,
860
+ latency: converted.latencyProfiles.find(p => p.inference_point_id === point.id) || null,
861
+ throughput: converted.throughputProfiles.find(p => p.inference_point_id === point.id) || null,
862
+ reliability: converted.reliabilityProfiles.find(p => p.inference_point_id === point.id) || null,
863
+ });
864
+ }
865
+ }
866
+
867
+ // Aggregate optimizations
868
+ const allOptimizations: StaticAnalysisOutput['all_optimizations'] = [];
869
+
870
+ for (const profile of allPerformanceProfiles) {
871
+ if (profile.cost?.optimizations) {
872
+ for (const opt of profile.cost.optimizations) {
873
+ allOptimizations.push({
874
+ dimension: 'cost',
875
+ inference_point_id: profile.inference_point_id,
876
+ file: profile.file,
877
+ line: profile.line,
878
+ type: opt.type,
879
+ description: opt.description,
880
+ impact: `${opt.savings_percent}% savings`,
881
+ effort: opt.effort,
882
+ priority: opt.savings_percent > 50 ? 'high' : opt.savings_percent > 20 ? 'medium' : 'low',
883
+ });
884
+ }
885
+ }
886
+
887
+ if (profile.latency?.optimizations) {
888
+ for (const opt of profile.latency.optimizations) {
889
+ allOptimizations.push({
890
+ dimension: 'latency',
891
+ inference_point_id: profile.inference_point_id,
892
+ file: profile.file,
893
+ line: profile.line,
894
+ type: opt.type,
895
+ description: opt.description,
896
+ impact: `${opt.improvement_percent}% improvement`,
897
+ effort: opt.effort,
898
+ priority: opt.improvement_percent > 50 ? 'high' : 'medium',
899
+ });
900
+ }
901
+ }
902
+
903
+ if (profile.throughput?.optimizations) {
904
+ for (const opt of profile.throughput.optimizations) {
905
+ allOptimizations.push({
906
+ dimension: 'throughput',
907
+ inference_point_id: profile.inference_point_id,
908
+ file: profile.file,
909
+ line: profile.line,
910
+ type: opt.type,
911
+ description: opt.description,
912
+ impact: opt.improvement,
913
+ effort: opt.effort,
914
+ priority: opt.type === 'add_rate_limiter' ? 'high' : 'medium',
915
+ });
916
+ }
917
+ }
918
+
919
+ if (profile.reliability?.optimizations) {
920
+ for (const opt of profile.reliability.optimizations) {
921
+ allOptimizations.push({
922
+ dimension: 'reliability',
923
+ inference_point_id: profile.inference_point_id,
924
+ file: profile.file,
925
+ line: profile.line,
926
+ type: opt.type,
927
+ description: opt.description,
928
+ impact: `${opt.reliability_before} → ${opt.reliability_after}`,
929
+ effort: opt.effort,
930
+ priority: opt.priority,
931
+ });
932
+ }
933
+ }
934
+ }
935
+
936
+ // Calculate summary
937
+ const allProviders = new Set<string>();
938
+ const allModels = new Set<string>();
939
+
940
+ for (const callsite of allCallsites) {
941
+ callsite.summary.providers_detected.forEach(p => allProviders.add(p));
942
+ callsite.summary.models_detected.forEach(m => allModels.add(m));
943
+ }
944
+
945
+ const totalCostPer1k = allCostAnalysis.reduce((sum, a) => sum + a.summary.estimated_cost_per_1k_calls, 0);
946
+ const maxP95 = Math.max(...allLatencyAnalysis.map(a => a.summary.estimated_p95_ms), 0);
947
+
948
+ // Determine overall reliability
949
+ const reliabilityLevels = allReliabilityAnalysis.map(a => a.summary.overall_reliability);
950
+ const reliabilityScore = reliabilityLevels.reduce((sum, level) => {
951
+ switch (level) {
952
+ case 'resilient': return sum + 4;
953
+ case 'robust': return sum + 3;
954
+ case 'moderate': return sum + 2;
955
+ case 'fragile': return sum + 1;
956
+ default: return sum;
957
+ }
958
+ }, 0);
959
+ const avgReliability = reliabilityLevels.length > 0 ? reliabilityScore / reliabilityLevels.length : 1;
960
+ let overallReliability = 'fragile';
961
+ if (avgReliability >= 3.5) overallReliability = 'resilient';
962
+ else if (avgReliability >= 2.5) overallReliability = 'robust';
963
+ else if (avgReliability >= 1.5) overallReliability = 'moderate';
964
+
965
+ return {
966
+ imports: allImports,
967
+ callsites: allCallsites,
968
+ performance_profiles: allPerformanceProfiles,
969
+ cost_analysis: allCostAnalysis,
970
+ latency_analysis: allLatencyAnalysis,
971
+ throughput_analysis: allThroughputAnalysis,
972
+ reliability_analysis: allReliabilityAnalysis,
973
+ summary: {
974
+ total_files: input.files.length,
975
+ total_inference_points: allPerformanceProfiles.length,
976
+ providers: [...allProviders],
977
+ models: [...allModels],
978
+
979
+ estimated_cost_per_1k_calls: totalCostPer1k,
980
+ cost_risk_high: allCostAnalysis.reduce((sum, a) =>
981
+ sum + a.cost_profiles.filter(p => p.cost_risk.level === 'high' || p.cost_risk.level === 'critical').length, 0),
982
+
983
+ blocking_calls: allLatencyAnalysis.reduce((sum, a) => sum + a.summary.blocking_calls, 0),
984
+ streaming_enabled: allLatencyAnalysis.reduce((sum, a) => sum + a.summary.streaming_enabled, 0),
985
+ estimated_p95_ms: maxP95,
986
+
987
+ has_rate_limiting: allThroughputAnalysis.reduce((sum, a) => sum + a.summary.has_rate_limiting, 0),
988
+ scaling_bottlenecks: allThroughputAnalysis.reduce((sum, a) => sum + a.summary.scaling_bottlenecks, 0),
989
+
990
+ has_error_handling: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.has_error_handling, 0),
991
+ has_retry: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.has_retry, 0),
992
+ has_fallback: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.has_fallback, 0),
993
+ anti_patterns_found: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.anti_patterns_found, 0),
994
+ overall_reliability: overallReliability,
995
+
996
+ total_optimizations: allOptimizations.length,
997
+ critical_optimizations: allOptimizations.filter(o => o.priority === 'critical' || o.priority === 'high').length,
998
+ },
999
+ all_optimizations: allOptimizations,
1000
+ insights: allInsights,
1001
+ };
1002
+ }
1003
+ }
1004
+
1005
+ // =============================================================================
1006
+ // CONVENIENCE FUNCTION
1007
+ // =============================================================================
1008
+
1009
+ export async function runStaticAnalysis(
1010
+ input: StaticAnalysisInput,
1011
+ anthropicKey?: string
1012
+ ): Promise<StaticAnalysisOutput> {
1013
+ const orchestrator = new StaticAnalysisOrchestrator(anthropicKey);
1014
+ return orchestrator.analyze(input);
1015
+ }