@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,692 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { query } from '@anthropic-ai/claude-agent-sdk';
4
+ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
5
+ import type { ScanResult, Callsite, Patterns, Provider } from './types.js';
6
+ import { createHash } from 'crypto';
7
+ import { loadPrompt, getDefaultPrompt, loadConfig, getConfiguredMode, isCascadeEnabled, type AnalysisPrompt } from './templates.js';
8
+ import { analyzeWithAgent, convertAgentCallsites } from './agent-analyzer.js';
9
+
10
+ // =============================================================================
11
+ // CONSTANTS
12
+ // =============================================================================
13
+
14
+ const LLM_BATCH_SIZE = 2; // Process files in small batches for smoother progress
15
+ const MAX_CONTEXT_CHARS = 4000; // Max chars per file to send to LLM
16
+
17
+ // Fallback regex patterns (used when LLM unavailable)
18
+ const PROVIDER_PATTERNS: Record<string, RegExp[]> = {
19
+ openai: [/openai/i, /\.chat\.completions\.create/, /\.embeddings\.create/, /from\s+['"]openai['"]/],
20
+ anthropic: [/anthropic/i, /\.messages\.create/, /from\s+['"]@anthropic-ai/],
21
+ google: [/google\.generative/i, /genai\./, /from\s+['"]@google\/generative/],
22
+ together: [/together/i, /Together\s*\(/, /from\s+['"]together/],
23
+ fireworks: [/fireworks/i, /Fireworks\s*\(/],
24
+ groq: [/groq/i, /Groq\s*\(/],
25
+ mistral: [/mistral/i, /Mistral\s*\(/],
26
+ cohere: [/cohere/i, /Cohere\s*\(/],
27
+ replicate: [/replicate/i, /Replicate\s*\(/],
28
+ aws_bedrock: [/bedrock/i, /BedrockRuntime/],
29
+ azure: [/azure.*openai/i, /AzureOpenAI/],
30
+ vllm: [/vllm/i, /from\s+vllm/],
31
+ sglang: [/sglang/i, /SGLang/],
32
+ ollama: [/ollama/i, /Ollama\s*\(/],
33
+ };
34
+
35
+ // Framework detection patterns
36
+ const FRAMEWORK_PATTERNS: Record<string, RegExp[]> = {
37
+ dspy: [/import\s+dspy/, /from\s+dspy/, /dspy\.Predict/, /dspy\.ChainOfThought/, /dspy\.LM\(/],
38
+ langchain: [/from\s+langchain/, /import\s+langchain/, /ChatOpenAI\(/, /LLMChain\(/],
39
+ llamaindex: [/from\s+llama_index/, /import\s+llama_index/, /llama_index\.llms/],
40
+ };
41
+
42
+ const MODEL_PATTERNS: RegExp[] = [
43
+ /model\s*[=:]\s*['"]([^'"]+)['"]/i,
44
+ /model_name\s*[=:]\s*['"]([^'"]+)['"]/i,
45
+ /modelId\s*[=:]\s*['"]([^'"]+)['"]/i,
46
+ // DSPy-style: dspy.LM("openai/gpt-4o-mini") or dspy.LM("anthropic/claude-3-5-sonnet")
47
+ /dspy\.LM\s*\(\s*['"](?:[\w-]+\/)?([^'"]+)['"]/i,
48
+ // Embeddings models
49
+ /embeddings\.create\([^)]*model\s*[=:]\s*['"]([^'"]+)['"]/i,
50
+ ];
51
+
52
+ const PATTERN_DETECTORS: Record<keyof Patterns, RegExp[]> = {
53
+ streaming: [/stream\s*[=:]\s*true/i, /\.stream\s*\(/, /for\s+await\s*\(/],
54
+ batching: [/batch/i, /Promise\.all/, /\.map\s*\(\s*async/],
55
+ retries: [/retry/i, /max_retries/i, /backoff/i],
56
+ caching: [/cache/i, /memoize/i, /redis/i],
57
+ fallback: [/fallback/i, /\.catch\s*\(/, /except\s*:/],
58
+ };
59
+
60
+ // =============================================================================
61
+ // TYPES
62
+ // =============================================================================
63
+
64
+ interface LLMCallsite {
65
+ line: number;
66
+ provider: string | null;
67
+ model: string | null;
68
+ framework: string | null;
69
+ patterns: {
70
+ streaming?: boolean;
71
+ batching?: boolean;
72
+ retries?: boolean;
73
+ caching?: boolean;
74
+ fallback?: boolean;
75
+ };
76
+ confidence: number;
77
+ reasoning: string;
78
+ }
79
+
80
+ // LLM-generated impact estimate (v1.8: 6-layer architecture)
81
+ export interface LLMImpactEstimate {
82
+ layer: 'application' | 'api' | 'gateway' | 'runtime' | 'model' | 'hardware';
83
+ impactType: 'cost' | 'latency' | 'throughput';
84
+ estimatedImpactPercent: number;
85
+ effort: 'low' | 'medium' | 'high';
86
+ }
87
+
88
+ // LLM-generated semantic insight (exported for use by agent)
89
+ export interface LLMInsight {
90
+ severity: 'critical' | 'warning' | 'info';
91
+ category: 'cost' | 'latency' | 'reliability' | 'waste' | 'security' | 'best-practice' | 'throughput';
92
+ headline: string;
93
+ evidence: string;
94
+ location: string; // file:line
95
+ recommendation?: string;
96
+ impact?: LLMImpactEstimate; // LLM-generated impact estimate
97
+ }
98
+
99
+ interface LLMAnalysisResult {
100
+ callsites: LLMCallsite[];
101
+ insights: LLMInsight[];
102
+ }
103
+
104
+ interface AnalyzeOptions {
105
+ useLLM?: boolean;
106
+ useAgent?: boolean; // Use agent-based analysis with tool use (Opus 4.5) for better accuracy
107
+ verbose?: boolean;
108
+ promptId?: string; // ID of the analysis prompt to use (defaults to 'peak-performance')
109
+ onProgress?: (data: { percent: number; currentFile?: string }) => void; // Progress callback
110
+ }
111
+
112
+ // =============================================================================
113
+ // HELPERS
114
+ // =============================================================================
115
+
116
+ function generateCallsiteId(file: string, line: number): string {
117
+ const hash = createHash('sha256')
118
+ .update(`${file}:${line}`)
119
+ .digest('hex')
120
+ .slice(0, 8);
121
+ return `cs_${hash}`;
122
+ }
123
+
124
+ function extractContext(content: string, line: number, windowSize: number = 10): string {
125
+ const lines = content.split('\n');
126
+ const start = Math.max(0, line - windowSize);
127
+ const end = Math.min(lines.length, line + windowSize);
128
+ return lines.slice(start, end).join('\n');
129
+ }
130
+
131
+ function truncateContent(content: string): string {
132
+ if (content.length <= MAX_CONTEXT_CHARS) return content;
133
+ return content.slice(0, MAX_CONTEXT_CHARS) + '\n// ... truncated ...';
134
+ }
135
+
136
+ // =============================================================================
137
+ // LLM ANALYSIS (Claude Agent SDK)
138
+ // =============================================================================
139
+
140
+ /**
141
+ * Extract text content from Claude Agent SDK messages
142
+ */
143
+ function extractTextFromMessages(messages: SDKMessage[]): string {
144
+ let text = '';
145
+ for (const msg of messages) {
146
+ if (msg.type === 'assistant' && msg.message?.content) {
147
+ for (const block of msg.message.content) {
148
+ if (block.type === 'text') {
149
+ text += block.text;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ return text;
155
+ }
156
+
157
+ // Legacy static analysis prompt (hardcoded fallback)
158
+ // NOTE: Primary analysis now uses unified-analyzer.yaml via StaticAnalysisOrchestrator
159
+ function getStaticAnalysisPrompt(): string {
160
+ return `You are an expert at analyzing code to identify LLM inference usage and potential issues.
161
+
162
+ Analyze the following code and:
163
+
164
+ ## PART 1: Identify LLM Usage
165
+ For each LLM API call, extract:
166
+ - line: The EXACT line number where the inference call is made (not client initialization)
167
+ - provider: MUST be one of: openai, anthropic, google, together, fireworks, groq, mistral, cohere, replicate, aws_bedrock, azure, vllm, sglang, ollama, unknown
168
+ - model: The EXACT model name as specified in the code (e.g., "gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet-20241022", "text-embedding-3-small")
169
+ - framework: langchain, llamaindex, dspy, or null
170
+ - patterns: streaming, batching, retries, caching, fallback (true/false)
171
+ - confidence: 0.0 to 1.0
172
+ - reasoning: Brief explanation
173
+
174
+ CRITICAL RULES FOR MODEL EXTRACTION:
175
+ 1. Look at the model= parameter in the SAME function call
176
+ 2. If model is a variable, trace it to find the string value
177
+ 3. For embeddings calls, use the embedding model name (e.g., "text-embedding-3-small"), NOT a chat model
178
+ 4. For DSPy: look at dspy.LM("provider/model") or dspy.context(lm=...) to find the model
179
+ 5. Return the FULL model name exactly as written (e.g., "gpt-4o-mini" not "gpt-4")
180
+
181
+ CRITICAL: DO NOT flag these as callsites:
182
+ - Client initialization: openai.OpenAI(), anthropic.Anthropic(), etc.
183
+ - Import statements
184
+ - Type annotations or comments
185
+ - Variable assignments without actual API calls
186
+
187
+ ## PART 2: Generate Insights
188
+ Identify potential issues, anti-patterns, or improvements.
189
+
190
+ IMPORTANT - Use these EXACT values:
191
+ - severity: MUST be one of: "critical", "warning", "info"
192
+ - category: MUST be one of: "cost", "latency", "reliability", "waste", "security", "best-practice"
193
+
194
+ Issues to look for:
195
+ - Missing error handling for LLM calls (reliability)
196
+ - Hardcoded API keys or secrets (security)
197
+ - Inefficient patterns - no batching, no streaming (latency)
198
+ - Model selection issues - overpowered model for simple tasks (cost)
199
+ - Missing retries for production code (reliability)
200
+ - Cost optimization opportunities (cost)
201
+
202
+ Return ONLY valid JSON:
203
+ {
204
+ "callsites": [
205
+ {
206
+ "line": 42,
207
+ "provider": "openai",
208
+ "model": "gpt-4o",
209
+ "framework": null,
210
+ "patterns": {"streaming": true, "batching": false, "retries": true, "caching": false, "fallback": true},
211
+ "confidence": 0.95,
212
+ "reasoning": "Direct OpenAI API call"
213
+ }
214
+ ],
215
+ "insights": [
216
+ {
217
+ "severity": "warning",
218
+ "category": "reliability",
219
+ "headline": "No retry logic for LLM call",
220
+ "evidence": "The API call at line 42 has no retry handling - LLM APIs can fail transiently",
221
+ "location": "src/chat.py:42",
222
+ "recommendation": "Add exponential backoff retry logic"
223
+ }
224
+ ]
225
+ }
226
+
227
+ If no issues found, return empty arrays: {"callsites": [], "insights": []}`;
228
+ }
229
+
230
+ interface LLMAnalysisOutput {
231
+ callsitesByFile: Map<string, LLMCallsite[]>;
232
+ insights: LLMInsight[];
233
+ }
234
+
235
+ // Normalize LLM insights to valid values
236
+ function normalizeInsight(insight: LLMInsight): LLMInsight {
237
+ // Normalize severity
238
+ const severityMap: Record<string, 'critical' | 'warning' | 'info'> = {
239
+ error: 'critical',
240
+ high: 'critical',
241
+ medium: 'warning',
242
+ low: 'info',
243
+ };
244
+ const severity = severityMap[insight.severity] || insight.severity;
245
+
246
+ // Normalize category
247
+ const categoryMap: Record<string, 'cost' | 'latency' | 'reliability' | 'waste' | 'security' | 'best-practice'> = {
248
+ cost_optimization: 'cost',
249
+ performance: 'latency',
250
+ efficiency: 'waste',
251
+ error_handling: 'reliability',
252
+ 'error-handling': 'reliability',
253
+ };
254
+ const category = categoryMap[insight.category] || insight.category;
255
+
256
+ return {
257
+ ...insight,
258
+ severity: severity as 'critical' | 'warning' | 'info',
259
+ category: category as 'cost' | 'latency' | 'reliability' | 'waste' | 'security' | 'best-practice',
260
+ };
261
+ }
262
+
263
+ async function analyzewithLLM(
264
+ files: Array<{ path: string; content: string; candidateLines: number[] }>,
265
+ analysisPrompt: string,
266
+ onProgress?: (data: { percent: number; currentFile?: string }) => void
267
+ ): Promise<LLMAnalysisOutput> {
268
+ const callsitesByFile = new Map<string, LLMCallsite[]>();
269
+ const allInsights: LLMInsight[] = [];
270
+ const totalFiles = files.length;
271
+ let llmErrorLogged = false; // Only log LLM errors once to avoid noise
272
+
273
+ // Process in batches
274
+ for (let i = 0; i < files.length; i += LLM_BATCH_SIZE) {
275
+ const batch = files.slice(i, i + LLM_BATCH_SIZE);
276
+ const currentFile = batch[0]?.path;
277
+
278
+ // Show progress bar BEFORE processing (visible during LLM call)
279
+ const percentBefore = Math.floor((i / totalFiles) * 100);
280
+ onProgress?.({ percent: percentBefore, currentFile });
281
+
282
+ const fileContents = batch.map(f => {
283
+ const truncated = truncateContent(f.content);
284
+ return `=== FILE: ${f.path} ===\nCandidate lines: ${f.candidateLines.join(', ')}\n\n${truncated}`;
285
+ }).join('\n\n');
286
+
287
+ try {
288
+ // Use Claude Agent SDK query() function
289
+ const agentQuery = query({
290
+ prompt: `${analysisPrompt}\n\n${fileContents}`,
291
+ options: {
292
+ model: 'claude-sonnet-4-20250514',
293
+ tools: [],
294
+ permissionMode: 'plan',
295
+ cwd: process.cwd(),
296
+ },
297
+ });
298
+
299
+ // Collect all messages from the async generator
300
+ const messages: SDKMessage[] = [];
301
+ for await (const message of agentQuery) {
302
+ messages.push(message);
303
+ }
304
+
305
+ // Extract text content from messages
306
+ const text = extractTextFromMessages(messages);
307
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
308
+
309
+ if (jsonMatch) {
310
+ const parsed = JSON.parse(jsonMatch[0]) as LLMAnalysisResult;
311
+
312
+ // Map callsites back to files
313
+ for (const callsite of parsed.callsites) {
314
+ for (const file of batch) {
315
+ if (file.candidateLines.includes(callsite.line)) {
316
+ const existing = callsitesByFile.get(file.path) || [];
317
+ existing.push(callsite);
318
+ callsitesByFile.set(file.path, existing);
319
+ break;
320
+ }
321
+ }
322
+ }
323
+
324
+ // Collect insights (normalized to valid values)
325
+ if (parsed.insights && Array.isArray(parsed.insights)) {
326
+ allInsights.push(...parsed.insights.map(normalizeInsight));
327
+ }
328
+ }
329
+ } catch (error) {
330
+ // Continue with regex fallback for this batch
331
+ // Only log once to avoid noisy output (Julie Zhou: calm, not alarming)
332
+ if (!llmErrorLogged) {
333
+ llmErrorLogged = true;
334
+ const errMsg = error instanceof Error ? error.message : String(error);
335
+ // Extract just the error type for cleaner output
336
+ const shortErr = errMsg.includes('authentication_error') ? 'invalid API key'
337
+ : errMsg.includes('rate_limit') ? 'rate limited'
338
+ : 'API error';
339
+ console.warn(`[analyzer] Claude Agent SDK unavailable (${shortErr}), using pattern matching`);
340
+ }
341
+ }
342
+
343
+ // Report progress AFTER batch completes
344
+ const processedFiles = Math.min(i + LLM_BATCH_SIZE, totalFiles);
345
+ const percentAfter = Math.floor((processedFiles / totalFiles) * 100);
346
+ onProgress?.({ percent: percentAfter, currentFile });
347
+ }
348
+
349
+ return { callsitesByFile, insights: allInsights };
350
+ }
351
+
352
+ // =============================================================================
353
+ // REGEX FALLBACK
354
+ // =============================================================================
355
+
356
+ function detectProviderRegex(context: string, fileContent: string): string | undefined {
357
+ for (const [provider, patterns] of Object.entries(PROVIDER_PATTERNS)) {
358
+ for (const pattern of patterns) {
359
+ if (pattern.test(context) || pattern.test(fileContent)) {
360
+ return provider;
361
+ }
362
+ }
363
+ }
364
+ return undefined;
365
+ }
366
+
367
+ function detectFrameworkRegex(context: string, fileContent: string): string | null {
368
+ for (const [framework, patterns] of Object.entries(FRAMEWORK_PATTERNS)) {
369
+ for (const pattern of patterns) {
370
+ if (pattern.test(context) || pattern.test(fileContent)) {
371
+ return framework;
372
+ }
373
+ }
374
+ }
375
+ return null;
376
+ }
377
+
378
+ function detectModelRegex(context: string): string | undefined {
379
+ for (const pattern of MODEL_PATTERNS) {
380
+ const match = context.match(pattern);
381
+ if (match && match[1]) {
382
+ return match[1];
383
+ }
384
+ }
385
+ return undefined;
386
+ }
387
+
388
+ function detectPatternsRegex(context: string): Patterns {
389
+ const patterns: Patterns = {};
390
+
391
+ for (const [pattern, regexes] of Object.entries(PATTERN_DETECTORS)) {
392
+ for (const regex of regexes) {
393
+ if (regex.test(context)) {
394
+ patterns[pattern as keyof Patterns] = true;
395
+ break;
396
+ }
397
+ }
398
+ }
399
+
400
+ return patterns;
401
+ }
402
+
403
+ function calculateConfidence(
404
+ hasProvider: boolean,
405
+ hasModel: boolean,
406
+ patternCount: number
407
+ ): number {
408
+ let confidence = 0.3;
409
+ if (hasProvider) confidence += 0.3;
410
+ if (hasModel) confidence += 0.25;
411
+ if (patternCount > 0) confidence += 0.05 * Math.min(patternCount, 3);
412
+ return Math.min(confidence, 1.0);
413
+ }
414
+
415
+ // =============================================================================
416
+ // PUBLIC API
417
+ // =============================================================================
418
+
419
+ /**
420
+ * Result of analyzing scan results
421
+ */
422
+ export interface AnalyzeResult {
423
+ callsites: Callsite[];
424
+ insights: LLMInsight[];
425
+ }
426
+
427
+ /**
428
+ * Analyze scan results to extract semantic information from callsites.
429
+ * Uses LLM for semantic analysis when ANTHROPIC_API_KEY is available,
430
+ * falls back to regex patterns otherwise.
431
+ *
432
+ * Returns both callsites AND LLM-generated semantic insights (phase 1).
433
+ * Template-based insights are generated separately (phase 2).
434
+ */
435
+ export async function analyze(
436
+ scanResult: ScanResult,
437
+ options: AnalyzeOptions = {}
438
+ ): Promise<AnalyzeResult> {
439
+ // Load config and determine analysis mode
440
+ const config = loadConfig();
441
+ const configuredMode = getConfiguredMode();
442
+ const cascadeEnabled = isCascadeEnabled();
443
+
444
+ // Options can override config, but config provides defaults
445
+ const {
446
+ useLLM = configuredMode === 'llm' || (configuredMode === 'agent' && cascadeEnabled),
447
+ useAgent = configuredMode === 'agent',
448
+ verbose = config.agent.verbose,
449
+ promptId,
450
+ onProgress
451
+ } = options;
452
+
453
+ if (verbose) {
454
+ console.log(`[analyzer] Mode: ${configuredMode}, Cascade: ${cascadeEnabled}`);
455
+ }
456
+
457
+ // First preference: Agent-based analysis (most accurate)
458
+ if (useAgent && process.env.ANTHROPIC_API_KEY) {
459
+ try {
460
+ if (verbose) {
461
+ console.log('[analyzer] Using agent-based analysis');
462
+ }
463
+ const agentResult = await analyzeWithAgent(scanResult, { verbose });
464
+ return {
465
+ callsites: convertAgentCallsites(agentResult.callsites),
466
+ insights: agentResult.insights as LLMInsight[],
467
+ };
468
+ } catch (error) {
469
+ if (cascadeEnabled) {
470
+ console.warn('[analyzer] Agent analysis failed, falling back to LLM/regex:', error);
471
+ // Fall through to LLM or regex analysis
472
+ } else {
473
+ throw error; // Don't cascade, re-throw
474
+ }
475
+ }
476
+ }
477
+
478
+ // Second preference: Single-prompt LLM analysis
479
+ // Third preference: Regex-only analysis (when useLLM=false or no API key)
480
+
481
+ const callsites: Callsite[] = [];
482
+ const llmInsights: LLMInsight[] = [];
483
+ const fileContents = new Map<string, string>();
484
+
485
+ // Load analysis prompt (from YAML config or fallback)
486
+ let analysisPromptText = getStaticAnalysisPrompt(); // Load from YAML or fallback
487
+ if (promptId) {
488
+ const customPrompt = loadPrompt(promptId);
489
+ if (customPrompt) {
490
+ analysisPromptText = customPrompt.prompt;
491
+ } else {
492
+ console.warn(`[analyzer] Prompt '${promptId}' not found, using default`);
493
+ }
494
+ } else {
495
+ // Try to load default peak-performance prompt
496
+ try {
497
+ const defaultPrompt = getDefaultPrompt();
498
+ analysisPromptText = defaultPrompt.prompt;
499
+ } catch {
500
+ // Use hardcoded fallback if prompts directory doesn't exist
501
+ }
502
+ }
503
+
504
+ // Read file contents
505
+ for (const file of scanResult.files) {
506
+ try {
507
+ const absPath = join(scanResult.root, file.path);
508
+ fileContents.set(file.path, readFileSync(absPath, 'utf-8'));
509
+ } catch {
510
+ continue;
511
+ }
512
+ }
513
+
514
+ // Group candidates by file
515
+ const candidatesByFile = new Map<string, number[]>();
516
+ for (const candidate of scanResult.candidates) {
517
+ const existing = candidatesByFile.get(candidate.file) || [];
518
+ existing.push(candidate.line);
519
+ candidatesByFile.set(candidate.file, existing);
520
+ }
521
+
522
+ // Try LLM analysis if API key available (Claude Agent SDK uses env var)
523
+ let llmOutput: LLMAnalysisOutput | null = null;
524
+
525
+ if (useLLM && process.env.ANTHROPIC_API_KEY) {
526
+ try {
527
+ const filesToAnalyze = Array.from(candidatesByFile.entries())
528
+ .filter(([path]) => fileContents.has(path))
529
+ .map(([path, lines]) => ({
530
+ path,
531
+ content: fileContents.get(path)!,
532
+ candidateLines: lines,
533
+ }));
534
+
535
+ if (filesToAnalyze.length > 0) {
536
+ llmOutput = await analyzewithLLM(filesToAnalyze, analysisPromptText, onProgress);
537
+ // Collect LLM-generated insights (phase 1)
538
+ llmInsights.push(...llmOutput.insights);
539
+ }
540
+ } catch (error) {
541
+ console.warn('[analyzer] Claude Agent SDK initialization failed, using regex fallback');
542
+ }
543
+ }
544
+
545
+ // Process each candidate
546
+ for (const candidate of scanResult.candidates) {
547
+ const content = fileContents.get(candidate.file);
548
+ if (!content) continue;
549
+
550
+ // Check if we have LLM results for this file/line
551
+ const llmCallsites = llmOutput?.callsitesByFile.get(candidate.file);
552
+ const llmMatch = llmCallsites?.find(c => c.line === candidate.line);
553
+
554
+ if (llmMatch) {
555
+ // Use LLM results
556
+ const typedProvider: Provider | null = llmMatch.provider as Provider | null;
557
+
558
+ callsites.push({
559
+ id: generateCallsiteId(candidate.file, candidate.line),
560
+ file: candidate.file,
561
+ line: candidate.line,
562
+ provider: typedProvider,
563
+ model: llmMatch.model,
564
+ framework: llmMatch.framework,
565
+ runtime: null,
566
+ patterns: llmMatch.patterns,
567
+ confidence: llmMatch.confidence,
568
+ });
569
+ } else {
570
+ // Fallback to regex analysis
571
+ const context = extractContext(content, candidate.line);
572
+ const provider = detectProviderRegex(context, content);
573
+ const model = detectModelRegex(context);
574
+ const framework = detectFrameworkRegex(context, content);
575
+ const patterns = detectPatternsRegex(context);
576
+
577
+ const patternCount = Object.values(patterns).filter(Boolean).length;
578
+ const confidence = calculateConfidence(!!provider, !!model, patternCount);
579
+
580
+ const typedProvider: Provider | null = provider as Provider | null ?? null;
581
+
582
+ callsites.push({
583
+ id: generateCallsiteId(candidate.file, candidate.line),
584
+ file: candidate.file,
585
+ line: candidate.line,
586
+ provider: typedProvider,
587
+ model: model ?? null,
588
+ framework: framework,
589
+ runtime: null,
590
+ patterns,
591
+ confidence,
592
+ });
593
+ }
594
+ }
595
+
596
+ // Sort by confidence descending
597
+ callsites.sort((a, b) => b.confidence - a.confidence);
598
+
599
+ return { callsites, insights: llmInsights };
600
+ }
601
+
602
+ /**
603
+ * Re-analyze a single file (for incremental updates)
604
+ */
605
+ export async function analyzeFile(
606
+ filePath: string,
607
+ content: string,
608
+ lines: number[],
609
+ options: AnalyzeOptions = {}
610
+ ): Promise<AnalyzeResult> {
611
+ const { useLLM = true, promptId } = options;
612
+ const callsites: Callsite[] = [];
613
+ const llmInsights: LLMInsight[] = [];
614
+
615
+ // Load analysis prompt (from YAML config or fallback)
616
+ let analysisPromptText = getStaticAnalysisPrompt();
617
+ if (promptId) {
618
+ const customPrompt = loadPrompt(promptId);
619
+ if (customPrompt) {
620
+ analysisPromptText = customPrompt.prompt;
621
+ }
622
+ } else {
623
+ try {
624
+ const defaultPrompt = getDefaultPrompt();
625
+ analysisPromptText = defaultPrompt.prompt;
626
+ } catch {
627
+ // Use hardcoded fallback
628
+ }
629
+ }
630
+
631
+ // Try LLM analysis (Claude Agent SDK uses env var)
632
+ let llmCallsites: LLMCallsite[] = [];
633
+
634
+ if (useLLM && process.env.ANTHROPIC_API_KEY) {
635
+ try {
636
+ const output = await analyzewithLLM(
637
+ [{ path: filePath, content, candidateLines: lines }],
638
+ analysisPromptText
639
+ );
640
+ llmCallsites = output.callsitesByFile.get(filePath) || [];
641
+ llmInsights.push(...output.insights);
642
+ } catch {
643
+ // Fall through to regex
644
+ }
645
+ }
646
+
647
+ for (const line of lines) {
648
+ const llmMatch = llmCallsites.find(c => c.line === line);
649
+
650
+ if (llmMatch) {
651
+ const typedProvider: Provider | null = llmMatch.provider as Provider | null;
652
+
653
+ callsites.push({
654
+ id: generateCallsiteId(filePath, line),
655
+ file: filePath,
656
+ line,
657
+ provider: typedProvider,
658
+ model: llmMatch.model,
659
+ framework: llmMatch.framework,
660
+ runtime: null,
661
+ patterns: llmMatch.patterns,
662
+ confidence: llmMatch.confidence,
663
+ });
664
+ } else {
665
+ // Regex fallback
666
+ const context = extractContext(content, line);
667
+ const provider = detectProviderRegex(context, content);
668
+ const model = detectModelRegex(context);
669
+ const framework = detectFrameworkRegex(context, content);
670
+ const patterns = detectPatternsRegex(context);
671
+
672
+ const patternCount = Object.values(patterns).filter(Boolean).length;
673
+ const confidence = calculateConfidence(!!provider, !!model, patternCount);
674
+
675
+ const typedProvider: Provider | null = provider as Provider | null ?? null;
676
+
677
+ callsites.push({
678
+ id: generateCallsiteId(filePath, line),
679
+ file: filePath,
680
+ line,
681
+ provider: typedProvider,
682
+ model: model ?? null,
683
+ framework: framework,
684
+ runtime: null,
685
+ patterns,
686
+ confidence,
687
+ });
688
+ }
689
+ }
690
+
691
+ return { callsites, insights: llmInsights };
692
+ }