@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.
- package/.claude/settings.local.json +8 -0
- package/.env.example +6 -0
- package/.github/workflows/peakinfer.yml +64 -0
- package/CHANGELOG.md +31 -0
- package/LICENSE +190 -0
- package/README.md +335 -0
- package/data/inferencemax.json +274 -0
- package/dist/agent-analyzer.d.ts +45 -0
- package/dist/agent-analyzer.d.ts.map +1 -0
- package/dist/agent-analyzer.js +374 -0
- package/dist/agent-analyzer.js.map +1 -0
- package/dist/agent.d.ts +76 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +965 -0
- package/dist/agent.js.map +1 -0
- package/dist/agents/correlation-analyzer.d.ts +34 -0
- package/dist/agents/correlation-analyzer.d.ts.map +1 -0
- package/dist/agents/correlation-analyzer.js +261 -0
- package/dist/agents/correlation-analyzer.js.map +1 -0
- package/dist/agents/index.d.ts +91 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +111 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/runtime-analyzer.d.ts +38 -0
- package/dist/agents/runtime-analyzer.d.ts.map +1 -0
- package/dist/agents/runtime-analyzer.js +244 -0
- package/dist/agents/runtime-analyzer.js.map +1 -0
- package/dist/analysis-types.d.ts +500 -0
- package/dist/analysis-types.d.ts.map +1 -0
- package/dist/analysis-types.js +11 -0
- package/dist/analysis-types.js.map +1 -0
- package/dist/analytics.d.ts +25 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +94 -0
- package/dist/analytics.js.map +1 -0
- package/dist/analyzer.d.ts +48 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +547 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/artifacts.d.ts +44 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +165 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/benchmarks/index.d.ts +88 -0
- package/dist/benchmarks/index.d.ts.map +1 -0
- package/dist/benchmarks/index.js +205 -0
- package/dist/benchmarks/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +427 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ci.d.ts +19 -0
- package/dist/commands/ci.d.ts.map +1 -0
- package/dist/commands/ci.js +253 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +249 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/demo.d.ts +15 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +106 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/export.d.ts +14 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +209 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/history.d.ts +15 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +389 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/template.d.ts +14 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/template.js +341 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/validate-map.d.ts +12 -0
- package/dist/commands/validate-map.d.ts.map +1 -0
- package/dist/commands/validate-map.js +274 -0
- package/dist/commands/validate-map.js.map +1 -0
- package/dist/commands/whatif.d.ts +17 -0
- package/dist/commands/whatif.d.ts.map +1 -0
- package/dist/commands/whatif.js +206 -0
- package/dist/commands/whatif.js.map +1 -0
- package/dist/comparison.d.ts +38 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +223 -0
- package/dist/comparison.js.map +1 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/connectors/helicone.d.ts +9 -0
- package/dist/connectors/helicone.d.ts.map +1 -0
- package/dist/connectors/helicone.js +106 -0
- package/dist/connectors/helicone.js.map +1 -0
- package/dist/connectors/index.d.ts +37 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +65 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/langsmith.d.ts +9 -0
- package/dist/connectors/langsmith.d.ts.map +1 -0
- package/dist/connectors/langsmith.js +122 -0
- package/dist/connectors/langsmith.js.map +1 -0
- package/dist/connectors/types.d.ts +83 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +98 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/cost-estimator.d.ts +46 -0
- package/dist/cost-estimator.d.ts.map +1 -0
- package/dist/cost-estimator.js +104 -0
- package/dist/cost-estimator.js.map +1 -0
- package/dist/costs.d.ts +57 -0
- package/dist/costs.d.ts.map +1 -0
- package/dist/costs.js +251 -0
- package/dist/costs.js.map +1 -0
- package/dist/counterfactuals.d.ts +29 -0
- package/dist/counterfactuals.d.ts.map +1 -0
- package/dist/counterfactuals.js +448 -0
- package/dist/counterfactuals.js.map +1 -0
- package/dist/enhancement-prompts.d.ts +41 -0
- package/dist/enhancement-prompts.d.ts.map +1 -0
- package/dist/enhancement-prompts.js +88 -0
- package/dist/enhancement-prompts.js.map +1 -0
- package/dist/envelopes.d.ts +20 -0
- package/dist/envelopes.d.ts.map +1 -0
- package/dist/envelopes.js +790 -0
- package/dist/envelopes.js.map +1 -0
- package/dist/format-normalizer.d.ts +71 -0
- package/dist/format-normalizer.d.ts.map +1 -0
- package/dist/format-normalizer.js +1331 -0
- package/dist/format-normalizer.js.map +1 -0
- package/dist/history.d.ts +79 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +313 -0
- package/dist/history.js.map +1 -0
- package/dist/html.d.ts +11 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +463 -0
- package/dist/html.js.map +1 -0
- package/dist/impact.d.ts +42 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +443 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/insights.d.ts +5 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +271 -0
- package/dist/insights.js.map +1 -0
- package/dist/joiner.d.ts +9 -0
- package/dist/joiner.d.ts.map +1 -0
- package/dist/joiner.js +247 -0
- package/dist/joiner.js.map +1 -0
- package/dist/orchestrator.d.ts +34 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +827 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/pdf.d.ts +26 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +84 -0
- package/dist/pdf.js.map +1 -0
- package/dist/prediction.d.ts +33 -0
- package/dist/prediction.d.ts.map +1 -0
- package/dist/prediction.js +316 -0
- package/dist/prediction.js.map +1 -0
- package/dist/prompts/loader.d.ts +38 -0
- package/dist/prompts/loader.d.ts.map +1 -0
- package/dist/prompts/loader.js +60 -0
- package/dist/prompts/loader.js.map +1 -0
- package/dist/renderer.d.ts +64 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +923 -0
- package/dist/renderer.js.map +1 -0
- package/dist/runid.d.ts +57 -0
- package/dist/runid.d.ts.map +1 -0
- package/dist/runid.js +199 -0
- package/dist/runid.js.map +1 -0
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +366 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scanner.d.ts +11 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +426 -0
- package/dist/scanner.js.map +1 -0
- package/dist/templates.d.ts +120 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +429 -0
- package/dist/templates.js.map +1 -0
- package/dist/tools/index.d.ts +153 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +177 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types.d.ts +3647 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +703 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/docs/demo-guide.md +423 -0
- package/docs/events-format.md +295 -0
- package/docs/inferencemap-spec.md +344 -0
- package/docs/migration-v2.md +293 -0
- package/fixtures/demo/precomputed.json +142 -0
- package/fixtures/demo-project/README.md +52 -0
- package/fixtures/demo-project/ai-service.ts +65 -0
- package/fixtures/demo-project/sample-events.jsonl +15 -0
- package/fixtures/demo-project/src/ai-service.ts +128 -0
- package/fixtures/demo-project/src/llm-client.ts +155 -0
- package/package.json +65 -0
- package/prompts/agent-analyzer.yaml +47 -0
- package/prompts/ci-gate.yaml +98 -0
- package/prompts/correlation-analyzer.yaml +178 -0
- package/prompts/format-normalizer.yaml +46 -0
- package/prompts/peak-performance.yaml +180 -0
- package/prompts/pr-comment.yaml +111 -0
- package/prompts/runtime-analyzer.yaml +189 -0
- package/prompts/unified-analyzer.yaml +241 -0
- package/schemas/inference-map.v0.1.json +215 -0
- package/scripts/benchmark.ts +394 -0
- package/scripts/demo-v1.5.sh +158 -0
- package/scripts/sync-from-site.sh +197 -0
- package/scripts/validate-sync.sh +178 -0
- package/src/agent-analyzer.ts +481 -0
- package/src/agent.ts +1232 -0
- package/src/agents/correlation-analyzer.ts +353 -0
- package/src/agents/index.ts +235 -0
- package/src/agents/runtime-analyzer.ts +343 -0
- package/src/analysis-types.ts +558 -0
- package/src/analytics.ts +100 -0
- package/src/analyzer.ts +692 -0
- package/src/artifacts.ts +218 -0
- package/src/benchmarks/index.ts +309 -0
- package/src/cli.ts +503 -0
- package/src/commands/ci.ts +336 -0
- package/src/commands/config.ts +288 -0
- package/src/commands/demo.ts +175 -0
- package/src/commands/export.ts +297 -0
- package/src/commands/history.ts +425 -0
- package/src/commands/template.ts +385 -0
- package/src/commands/validate-map.ts +324 -0
- package/src/commands/whatif.ts +272 -0
- package/src/comparison.ts +283 -0
- package/src/config.ts +188 -0
- package/src/connectors/helicone.ts +164 -0
- package/src/connectors/index.ts +93 -0
- package/src/connectors/langsmith.ts +179 -0
- package/src/connectors/types.ts +180 -0
- package/src/cost-estimator.ts +146 -0
- package/src/costs.ts +347 -0
- package/src/counterfactuals.ts +516 -0
- package/src/enhancement-prompts.ts +118 -0
- package/src/envelopes.ts +814 -0
- package/src/format-normalizer.ts +1486 -0
- package/src/history.ts +400 -0
- package/src/html.ts +512 -0
- package/src/impact.ts +522 -0
- package/src/index.ts +83 -0
- package/src/insights.ts +341 -0
- package/src/joiner.ts +289 -0
- package/src/orchestrator.ts +1015 -0
- package/src/pdf.ts +110 -0
- package/src/prediction.ts +392 -0
- package/src/prompts/loader.ts +88 -0
- package/src/renderer.ts +1045 -0
- package/src/runid.ts +261 -0
- package/src/runtime.ts +450 -0
- package/src/scanner.ts +508 -0
- package/src/templates.ts +561 -0
- package/src/tools/index.ts +214 -0
- package/src/types.ts +873 -0
- package/src/version.ts +24 -0
- package/templates/context-accumulation.yaml +23 -0
- package/templates/cost-concentration.yaml +20 -0
- package/templates/dead-code.yaml +20 -0
- package/templates/latency-explainer.yaml +23 -0
- package/templates/optimizations/ab-testing-framework.yaml +74 -0
- package/templates/optimizations/api-gateway-optimization.yaml +81 -0
- package/templates/optimizations/api-model-routing-strategy.yaml +126 -0
- package/templates/optimizations/auto-scaling-optimization.yaml +85 -0
- package/templates/optimizations/batch-utilization-diagnostic.yaml +142 -0
- package/templates/optimizations/comprehensive-apm.yaml +76 -0
- package/templates/optimizations/context-window-optimization.yaml +91 -0
- package/templates/optimizations/cost-sensitive-batch-processing.yaml +77 -0
- package/templates/optimizations/distributed-training-optimization.yaml +77 -0
- package/templates/optimizations/document-analysis-edge.yaml +77 -0
- package/templates/optimizations/document-pipeline-optimization.yaml +78 -0
- package/templates/optimizations/domain-specific-distillation.yaml +78 -0
- package/templates/optimizations/error-handling-optimization.yaml +76 -0
- package/templates/optimizations/gptq-4bit-quantization.yaml +96 -0
- package/templates/optimizations/long-context-memory-management.yaml +78 -0
- package/templates/optimizations/max-tokens-optimization.yaml +76 -0
- package/templates/optimizations/memory-bandwidth-optimization.yaml +73 -0
- package/templates/optimizations/multi-framework-resilience.yaml +75 -0
- package/templates/optimizations/multi-tenant-optimization.yaml +75 -0
- package/templates/optimizations/prompt-caching-optimization.yaml +143 -0
- package/templates/optimizations/pytorch-to-onnx-migration.yaml +109 -0
- package/templates/optimizations/quality-monitoring.yaml +74 -0
- package/templates/optimizations/realtime-budget-controls.yaml +74 -0
- package/templates/optimizations/realtime-latency-optimization.yaml +74 -0
- package/templates/optimizations/sglang-concurrency-optimization.yaml +78 -0
- package/templates/optimizations/smart-model-routing.yaml +96 -0
- package/templates/optimizations/streaming-batch-selection.yaml +167 -0
- package/templates/optimizations/system-prompt-optimization.yaml +75 -0
- package/templates/optimizations/tensorrt-llm-performance.yaml +77 -0
- package/templates/optimizations/vllm-high-throughput-optimization.yaml +93 -0
- package/templates/optimizations/vllm-migration-memory-bound.yaml +78 -0
- package/templates/overpowered-extraction.yaml +32 -0
- package/templates/overpowered-model.yaml +31 -0
- package/templates/prompt-bloat.yaml +24 -0
- package/templates/retry-explosion.yaml +28 -0
- package/templates/schema/insight.schema.json +113 -0
- package/templates/schema/optimization.schema.json +180 -0
- package/templates/streaming-drift.yaml +30 -0
- package/templates/throughput-gap.yaml +21 -0
- package/templates/token-underutilization.yaml +28 -0
- package/templates/untested-fallback.yaml +21 -0
- package/tests/accuracy/drift-detection.test.ts +184 -0
- package/tests/accuracy/false-positives.test.ts +166 -0
- package/tests/accuracy/templates.test.ts +205 -0
- package/tests/action/commands.test.ts +125 -0
- package/tests/action/comments.test.ts +347 -0
- package/tests/cli.test.ts +203 -0
- package/tests/comparison.test.ts +309 -0
- package/tests/correlation-analyzer.test.ts +534 -0
- package/tests/counterfactuals.test.ts +347 -0
- package/tests/fixtures/events/missing-id.jsonl +1 -0
- package/tests/fixtures/events/missing-input.jsonl +1 -0
- package/tests/fixtures/events/missing-latency.jsonl +1 -0
- package/tests/fixtures/events/missing-model.jsonl +1 -0
- package/tests/fixtures/events/missing-output.jsonl +1 -0
- package/tests/fixtures/events/missing-provider.jsonl +1 -0
- package/tests/fixtures/events/missing-ts.jsonl +1 -0
- package/tests/fixtures/events/valid.csv +3 -0
- package/tests/fixtures/events/valid.json +1 -0
- package/tests/fixtures/events/valid.jsonl +2 -0
- package/tests/fixtures/events/with-callsite.jsonl +1 -0
- package/tests/fixtures/events/with-intent.jsonl +1 -0
- package/tests/fixtures/events/wrong-type.jsonl +1 -0
- package/tests/fixtures/repos/empty/.gitkeep +0 -0
- package/tests/fixtures/repos/hybrid-router/router.py +35 -0
- package/tests/fixtures/repos/saas-anthropic/agent.ts +27 -0
- package/tests/fixtures/repos/saas-openai/assistant.js +33 -0
- package/tests/fixtures/repos/saas-openai/client.py +26 -0
- package/tests/fixtures/repos/self-hosted-vllm/inference.py +22 -0
- package/tests/github-action.test.ts +292 -0
- package/tests/insights.test.ts +878 -0
- package/tests/joiner.test.ts +168 -0
- package/tests/performance/action-latency.test.ts +132 -0
- package/tests/performance/benchmark.test.ts +189 -0
- package/tests/performance/cli-latency.test.ts +102 -0
- package/tests/pr-comment.test.ts +313 -0
- package/tests/prediction.test.ts +296 -0
- package/tests/runtime-analyzer.test.ts +375 -0
- package/tests/runtime.test.ts +205 -0
- package/tests/scanner.test.ts +122 -0
- package/tests/template-conformance.test.ts +526 -0
- package/tests/unit/cost-calculator.test.ts +303 -0
- package/tests/unit/credits.test.ts +180 -0
- package/tests/unit/inference-map.test.ts +276 -0
- package/tests/unit/schema.test.ts +300 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,827 @@
|
|
|
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
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
18
|
+
import { EventEmitter } from 'events';
|
|
19
|
+
// Increase max listeners to handle parallel file analysis without warnings
|
|
20
|
+
// Each query() call adds exit listeners; with many files we exceed the default of 10
|
|
21
|
+
// Set to 500 to support large codebases (300+ files)
|
|
22
|
+
const originalMaxListeners = EventEmitter.defaultMaxListeners;
|
|
23
|
+
EventEmitter.defaultMaxListeners = 500;
|
|
24
|
+
process.setMaxListeners(500);
|
|
25
|
+
import { getUnifiedAnalyzerPrompt, formatUserMessage } from './prompts/loader.js';
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// LOAD PROMPT FROM CONFIG
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Always load fresh from YAML config (no caching for serverless)
|
|
30
|
+
function getPrompt() {
|
|
31
|
+
return getUnifiedAnalyzerPrompt();
|
|
32
|
+
}
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// HELPERS
|
|
35
|
+
// =============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Generate stable inference point ID using file:line format.
|
|
38
|
+
* This ensures IDs are consistent across runs for the same code location.
|
|
39
|
+
* Falls back to random ID only if no location info is available.
|
|
40
|
+
*/
|
|
41
|
+
function generatePointId(filePath, line) {
|
|
42
|
+
if (filePath && line) {
|
|
43
|
+
// Use stable file:line format (PRD requirement for consistent IDs)
|
|
44
|
+
return `${filePath}:${line}`;
|
|
45
|
+
}
|
|
46
|
+
// Fallback for rare cases where location is unknown
|
|
47
|
+
return 'pt_' + Math.random().toString(36).substring(2, 10);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Extract complete JSON object from text by matching brackets.
|
|
51
|
+
* This is more robust than regex which can match incomplete JSON.
|
|
52
|
+
* Returns null if the extracted JSON is too short or malformed.
|
|
53
|
+
*
|
|
54
|
+
* Handles common LLM issues:
|
|
55
|
+
* - Skips code snippets that aren't analysis results
|
|
56
|
+
* - Validates that response looks like analysis JSON (has "inference_points")
|
|
57
|
+
*/
|
|
58
|
+
function extractJSON(text) {
|
|
59
|
+
// Look for JSON that starts with {"inference_points" - the expected response format
|
|
60
|
+
// This avoids picking up code snippets that happen to start with {
|
|
61
|
+
const jsonStartPattern = /\{\s*"inference_points"/;
|
|
62
|
+
const match = text.match(jsonStartPattern);
|
|
63
|
+
if (!match || match.index === undefined) {
|
|
64
|
+
// Fallback: try to find any JSON object, but validate later
|
|
65
|
+
const fallbackStart = text.indexOf('{"');
|
|
66
|
+
if (fallbackStart === -1)
|
|
67
|
+
return null;
|
|
68
|
+
const extracted = extractJSONFromPosition(text, fallbackStart);
|
|
69
|
+
if (extracted && isValidAnalysisJSON(extracted)) {
|
|
70
|
+
return extracted;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return extractJSONFromPosition(text, match.index);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract JSON starting from a given position using bracket matching.
|
|
78
|
+
*/
|
|
79
|
+
function extractJSONFromPosition(text, start) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let inString = false;
|
|
82
|
+
let escape = false;
|
|
83
|
+
for (let i = start; i < text.length; i++) {
|
|
84
|
+
const char = text[i];
|
|
85
|
+
if (escape) {
|
|
86
|
+
escape = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (char === '\\' && inString) {
|
|
90
|
+
escape = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === '"' && !escape) {
|
|
94
|
+
inString = !inString;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (inString)
|
|
98
|
+
continue;
|
|
99
|
+
if (char === '{')
|
|
100
|
+
depth++;
|
|
101
|
+
if (char === '}') {
|
|
102
|
+
depth--;
|
|
103
|
+
if (depth === 0) {
|
|
104
|
+
const extracted = text.substring(start, i + 1);
|
|
105
|
+
// Sanity check: valid analysis JSON should be at least 50 chars
|
|
106
|
+
if (extracted.length < 50) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return extracted;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if extracted text looks like valid analysis JSON.
|
|
117
|
+
* Rejects code snippets and placeholder responses.
|
|
118
|
+
*/
|
|
119
|
+
function isValidAnalysisJSON(text) {
|
|
120
|
+
// Reject if it contains placeholder syntax like [...] or {...}
|
|
121
|
+
if (/\[\.\.\.\]|\{\.\.\.\}/.test(text)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
// Reject if it looks like code (unquoted keys with single quotes or variable names)
|
|
125
|
+
// Valid JSON has "key": not key: or 'key':
|
|
126
|
+
if (/[{,]\s*[a-zA-Z_][a-zA-Z0-9_]*\s*:/.test(text)) {
|
|
127
|
+
// Check if this is actually unquoted keys (not inside a string)
|
|
128
|
+
// Simple heuristic: if we see word: followed by non-string value, it's likely code
|
|
129
|
+
if (/[{,]\s*[a-zA-Z_]\w*\s*:\s*[a-zA-Z_]/.test(text)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Must contain "inference_points" for it to be a valid response
|
|
134
|
+
if (!text.includes('"inference_points"')) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function detectLanguage(filePath) {
|
|
140
|
+
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
|
141
|
+
const languageMap = {
|
|
142
|
+
'py': 'python',
|
|
143
|
+
'js': 'javascript',
|
|
144
|
+
'ts': 'typescript',
|
|
145
|
+
'tsx': 'typescript',
|
|
146
|
+
'jsx': 'javascript',
|
|
147
|
+
'go': 'go',
|
|
148
|
+
'rs': 'rust',
|
|
149
|
+
'java': 'java',
|
|
150
|
+
'rb': 'ruby',
|
|
151
|
+
'php': 'php',
|
|
152
|
+
};
|
|
153
|
+
return languageMap[ext] || 'unknown';
|
|
154
|
+
}
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// UNIFIED SINGLE-CALL ANALYSIS (Claude Agent SDK)
|
|
157
|
+
// =============================================================================
|
|
158
|
+
/**
|
|
159
|
+
* Extract text content from Claude Agent SDK messages
|
|
160
|
+
*/
|
|
161
|
+
function extractTextFromMessages(messages) {
|
|
162
|
+
let text = '';
|
|
163
|
+
for (const msg of messages) {
|
|
164
|
+
if (msg.type === 'assistant' && msg.message?.content) {
|
|
165
|
+
for (const block of msg.message.content) {
|
|
166
|
+
if (block.type === 'text') {
|
|
167
|
+
text += block.text;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return text;
|
|
173
|
+
}
|
|
174
|
+
async function runUnifiedAnalysis(filePath, content, language, _anthropicKey // Kept for signature compatibility, SDK uses env var
|
|
175
|
+
) {
|
|
176
|
+
// Load prompt from config file
|
|
177
|
+
const configPrompt = getPrompt();
|
|
178
|
+
// Format user message using template from config
|
|
179
|
+
const userMessage = configPrompt.userTemplate
|
|
180
|
+
? formatUserMessage(configPrompt.userTemplate, {
|
|
181
|
+
language,
|
|
182
|
+
file_path: filePath,
|
|
183
|
+
content,
|
|
184
|
+
})
|
|
185
|
+
: `Analyze this ${language} file for LLM inference points and their performance characteristics:
|
|
186
|
+
|
|
187
|
+
File: ${filePath}
|
|
188
|
+
|
|
189
|
+
\`\`\`${language}
|
|
190
|
+
${content}
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
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.`;
|
|
194
|
+
try {
|
|
195
|
+
// Use Claude Agent SDK query() function (per TDD v1.9.3)
|
|
196
|
+
const agentQuery = query({
|
|
197
|
+
prompt: userMessage,
|
|
198
|
+
options: {
|
|
199
|
+
systemPrompt: configPrompt.system,
|
|
200
|
+
model: 'claude-sonnet-4-20250514',
|
|
201
|
+
// Disable tools - we just want analysis output
|
|
202
|
+
tools: [],
|
|
203
|
+
// Use plan mode to avoid file operations
|
|
204
|
+
permissionMode: 'plan',
|
|
205
|
+
// Set working directory
|
|
206
|
+
cwd: process.cwd(),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
// Collect all messages from the async generator
|
|
210
|
+
const messages = [];
|
|
211
|
+
for await (const message of agentQuery) {
|
|
212
|
+
messages.push(message);
|
|
213
|
+
}
|
|
214
|
+
// Extract text content from messages
|
|
215
|
+
const responseText = extractTextFromMessages(messages);
|
|
216
|
+
if (responseText) {
|
|
217
|
+
// Use robust JSON extraction instead of greedy regex
|
|
218
|
+
const jsonStr = extractJSON(responseText);
|
|
219
|
+
if (jsonStr) {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(jsonStr);
|
|
222
|
+
// Validate that we got a proper response structure
|
|
223
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
224
|
+
// Silent: file probably doesn't have LLM calls
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
// Ensure inference_points array exists (even if empty)
|
|
228
|
+
if (!parsed.inference_points) {
|
|
229
|
+
parsed.inference_points = [];
|
|
230
|
+
}
|
|
231
|
+
// Ensure imports object exists
|
|
232
|
+
if (!parsed.imports) {
|
|
233
|
+
parsed.imports = { llm_providers: [], frameworks: [] };
|
|
234
|
+
}
|
|
235
|
+
// Ensure IDs are set using stable file:line format
|
|
236
|
+
for (const point of parsed.inference_points) {
|
|
237
|
+
if (!point.id) {
|
|
238
|
+
point.id = generatePointId(filePath, point.line);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return parsed;
|
|
242
|
+
}
|
|
243
|
+
catch (_parseError) {
|
|
244
|
+
// JSON extraction found something but it wasn't valid JSON
|
|
245
|
+
// This is expected for files without LLM calls - LLM may return code snippets
|
|
246
|
+
// Only log in verbose mode or if debugging is needed
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// No valid JSON found - this is normal for files without LLM inference points
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
// Only log actual SDK errors, not expected "no inference points" cases
|
|
255
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
256
|
+
if (!errorMsg.includes('rate limit') && !errorMsg.includes('timeout')) {
|
|
257
|
+
// Silent for most errors - files without LLM calls are expected
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.error('Claude Agent SDK error:', errorMsg);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// CONVERT UNIFIED TO LEGACY FORMAT
|
|
267
|
+
// =============================================================================
|
|
268
|
+
function convertUnifiedToLegacy(unified, filePath) {
|
|
269
|
+
const providers = unified.imports?.llm_providers || [];
|
|
270
|
+
const frameworks = unified.imports?.frameworks || [];
|
|
271
|
+
const imports = {
|
|
272
|
+
sdks: providers.map((p, i) => ({
|
|
273
|
+
name: p,
|
|
274
|
+
provider: p,
|
|
275
|
+
import_line: i + 1,
|
|
276
|
+
alias: null,
|
|
277
|
+
confidence: 0.9,
|
|
278
|
+
})),
|
|
279
|
+
frameworks: frameworks.map((f, i) => ({
|
|
280
|
+
name: f,
|
|
281
|
+
import_line: i + 1,
|
|
282
|
+
components: [],
|
|
283
|
+
confidence: 0.9,
|
|
284
|
+
})),
|
|
285
|
+
custom_wrappers: [],
|
|
286
|
+
infrastructure: [],
|
|
287
|
+
summary: {
|
|
288
|
+
has_llm_usage: providers.length > 0 || frameworks.length > 0,
|
|
289
|
+
primary_provider: providers[0] || null,
|
|
290
|
+
framework: frameworks[0] || null,
|
|
291
|
+
complexity: providers.length > 1 ? 'complex' : providers.length > 0 ? 'moderate' : 'simple',
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
const inferencePoints = unified.inference_points.map(p => ({
|
|
295
|
+
id: p.id,
|
|
296
|
+
line: p.line,
|
|
297
|
+
column: 0,
|
|
298
|
+
function_context: '',
|
|
299
|
+
class_context: null,
|
|
300
|
+
call_expression: '',
|
|
301
|
+
call_type: (p.call_type || 'direct'),
|
|
302
|
+
provider: { value: p.provider, source: 'hardcoded', confidence: 0.9 },
|
|
303
|
+
model: { value: p.model, source: 'hardcoded', confidence: 0.8 },
|
|
304
|
+
is_async: false,
|
|
305
|
+
in_loop: false,
|
|
306
|
+
loop_type: 'none',
|
|
307
|
+
estimated_calls: 'single',
|
|
308
|
+
needs_tracing: false,
|
|
309
|
+
confidence: 0.85,
|
|
310
|
+
}));
|
|
311
|
+
const callsites = {
|
|
312
|
+
inference_points: inferencePoints,
|
|
313
|
+
wrapper_definitions: [],
|
|
314
|
+
summary: {
|
|
315
|
+
total_inference_points: inferencePoints.length,
|
|
316
|
+
direct_calls: inferencePoints.filter(p => p.call_type === 'direct').length,
|
|
317
|
+
wrapped_calls: inferencePoints.filter(p => p.call_type === 'wrapper').length,
|
|
318
|
+
framework_calls: inferencePoints.filter(p => p.call_type === 'framework').length,
|
|
319
|
+
providers_detected: [...new Set(inferencePoints.map(p => p.provider.value))],
|
|
320
|
+
models_detected: [...new Set(inferencePoints.map(p => p.model.value).filter(Boolean))],
|
|
321
|
+
has_dynamic_routing: false,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
const costProfiles = unified.inference_points.map(p => ({
|
|
325
|
+
inference_point_id: p.id,
|
|
326
|
+
line: p.line,
|
|
327
|
+
model_analysis: {
|
|
328
|
+
model: p.model || 'unknown',
|
|
329
|
+
tier: (p.cost_profile?.tier || 'unknown'),
|
|
330
|
+
pricing: { input_per_1m: 0, output_per_1m: 0 },
|
|
331
|
+
is_overqualified: false,
|
|
332
|
+
reason: null,
|
|
333
|
+
},
|
|
334
|
+
token_estimates: {
|
|
335
|
+
input: { min: 100, typical: 500, max: 2000, basis: 'estimate' },
|
|
336
|
+
output: { min: 50, typical: 200, max: 1000, basis: 'estimate' },
|
|
337
|
+
has_few_shot: false,
|
|
338
|
+
few_shot_tokens: 0,
|
|
339
|
+
has_rag_context: false,
|
|
340
|
+
rag_context_estimate: 0,
|
|
341
|
+
},
|
|
342
|
+
call_frequency: { pattern: 'single', multiplier: 1, loop_bound: 'bounded', estimated_calls_per_invocation: 1 },
|
|
343
|
+
cost_estimate: {
|
|
344
|
+
per_call_min: p.cost_profile?.estimated_cost_per_call || 0,
|
|
345
|
+
per_call_typical: p.cost_profile?.estimated_cost_per_call || 0,
|
|
346
|
+
per_call_max: p.cost_profile?.estimated_cost_per_call || 0,
|
|
347
|
+
currency: 'USD',
|
|
348
|
+
},
|
|
349
|
+
cost_risk: { level: 'low', factors: [], unbounded_growth: false, context_accumulation: false },
|
|
350
|
+
optimizations: (p.cost_profile?.optimizations || []).map(o => ({
|
|
351
|
+
type: o.type,
|
|
352
|
+
description: o.description,
|
|
353
|
+
current_cost: '',
|
|
354
|
+
optimized_cost: '',
|
|
355
|
+
savings_percent: o.savings_percent,
|
|
356
|
+
effort: 'medium',
|
|
357
|
+
sample_change: null,
|
|
358
|
+
})),
|
|
359
|
+
confidence: 0.85,
|
|
360
|
+
}));
|
|
361
|
+
const latencyProfiles = unified.inference_points.map(p => ({
|
|
362
|
+
inference_point_id: p.id,
|
|
363
|
+
line: p.line,
|
|
364
|
+
blocking_analysis: {
|
|
365
|
+
is_blocking: p.latency_profile?.is_blocking || false,
|
|
366
|
+
is_in_request_handler: false,
|
|
367
|
+
blocks_event_loop: p.latency_profile?.is_blocking || false,
|
|
368
|
+
handler_type: 'unknown',
|
|
369
|
+
user_facing: false,
|
|
370
|
+
},
|
|
371
|
+
streaming_analysis: {
|
|
372
|
+
streaming_enabled: p.latency_profile?.has_streaming || false,
|
|
373
|
+
should_enable_streaming: !p.latency_profile?.has_streaming,
|
|
374
|
+
reason: p.latency_profile?.has_streaming ? 'Streaming already enabled' : 'Enable streaming for better UX',
|
|
375
|
+
time_to_first_token_benefit: !p.latency_profile?.has_streaming ? '~200ms vs full wait' : null,
|
|
376
|
+
},
|
|
377
|
+
async_analysis: {
|
|
378
|
+
is_async: false,
|
|
379
|
+
uses_await: false,
|
|
380
|
+
could_be_async: true,
|
|
381
|
+
async_benefit: 'Enable concurrency',
|
|
382
|
+
},
|
|
383
|
+
parallel_analysis: {
|
|
384
|
+
has_parallel_potential: false,
|
|
385
|
+
independent_calls: 0,
|
|
386
|
+
current_pattern: 'sequential',
|
|
387
|
+
parallelizable_calls: [],
|
|
388
|
+
parallel_speedup_estimate: null,
|
|
389
|
+
},
|
|
390
|
+
chain_analysis: {
|
|
391
|
+
chain_depth: 1,
|
|
392
|
+
sequential_calls: 1,
|
|
393
|
+
total_latency_estimate: { min_ms: 500, typical_ms: 2000, max_ms: 5000 },
|
|
394
|
+
chain_pattern: 'single',
|
|
395
|
+
},
|
|
396
|
+
timeout_analysis: {
|
|
397
|
+
timeout_configured: p.reliability_profile?.has_timeout || false,
|
|
398
|
+
timeout_value_ms: null,
|
|
399
|
+
has_fallback_on_timeout: false,
|
|
400
|
+
timeout_risk: p.reliability_profile?.has_timeout ? 'low' : 'high',
|
|
401
|
+
},
|
|
402
|
+
latency_risk: { level: 'medium', factors: [], tail_latency_risk: true, unpredictable: false },
|
|
403
|
+
latency_estimate: {
|
|
404
|
+
min_ms: 500,
|
|
405
|
+
typical_ms: 2000,
|
|
406
|
+
p95_ms: p.latency_profile?.estimated_p95_ms || 5000,
|
|
407
|
+
max_ms: 15000,
|
|
408
|
+
basis: 'Model estimate',
|
|
409
|
+
},
|
|
410
|
+
optimizations: (p.latency_profile?.optimizations || []).map(o => ({
|
|
411
|
+
type: o.type,
|
|
412
|
+
description: o.description,
|
|
413
|
+
current_latency: '',
|
|
414
|
+
optimized_latency: '',
|
|
415
|
+
improvement_percent: o.improvement_percent,
|
|
416
|
+
effort: 'medium',
|
|
417
|
+
sample_change: null,
|
|
418
|
+
})),
|
|
419
|
+
confidence: 0.85,
|
|
420
|
+
}));
|
|
421
|
+
const throughputProfiles = unified.inference_points.map(p => ({
|
|
422
|
+
inference_point_id: p.id,
|
|
423
|
+
line: p.line,
|
|
424
|
+
concurrency_analysis: {
|
|
425
|
+
concurrency_limit: null,
|
|
426
|
+
limit_source: 'none',
|
|
427
|
+
limit_location: null,
|
|
428
|
+
is_global_limit: false,
|
|
429
|
+
recommended_limit: 10,
|
|
430
|
+
},
|
|
431
|
+
rate_limiting: {
|
|
432
|
+
has_rate_limiter: p.throughput_profile?.has_rate_limiting || false,
|
|
433
|
+
rate_limit_type: 'none',
|
|
434
|
+
requests_per_minute: null,
|
|
435
|
+
handles_429: false,
|
|
436
|
+
backoff_strategy: 'none',
|
|
437
|
+
},
|
|
438
|
+
batching_analysis: {
|
|
439
|
+
batching_enabled: p.throughput_profile?.has_batching || false,
|
|
440
|
+
batch_size: null,
|
|
441
|
+
could_batch: !p.throughput_profile?.has_batching,
|
|
442
|
+
batching_benefit: 'Reduce API calls',
|
|
443
|
+
batch_api_available: true,
|
|
444
|
+
},
|
|
445
|
+
queue_analysis: { uses_queue: false, queue_type: 'none', async_processing: false, worker_pattern: false },
|
|
446
|
+
scaling_analysis: {
|
|
447
|
+
horizontally_scalable: true,
|
|
448
|
+
bottlenecks: (p.throughput_profile?.bottlenecks || []).map(b => ({
|
|
449
|
+
type: 'shared_state',
|
|
450
|
+
location: filePath,
|
|
451
|
+
description: b.description,
|
|
452
|
+
severity: 'medium',
|
|
453
|
+
})),
|
|
454
|
+
stateless: true,
|
|
455
|
+
client_reuse: false,
|
|
456
|
+
},
|
|
457
|
+
capacity_estimate: { max_concurrent_calls: 10, estimated_rps: 10, limiting_factor: 'API quota' },
|
|
458
|
+
throughput_risk: {
|
|
459
|
+
level: 'medium',
|
|
460
|
+
factors: [],
|
|
461
|
+
will_hit_rate_limits: !p.throughput_profile?.has_rate_limiting,
|
|
462
|
+
scaling_blocked: false,
|
|
463
|
+
},
|
|
464
|
+
optimizations: (p.throughput_profile?.optimizations || []).map(o => ({
|
|
465
|
+
type: o.type,
|
|
466
|
+
description: o.description,
|
|
467
|
+
current_throughput: '',
|
|
468
|
+
optimized_throughput: '',
|
|
469
|
+
improvement: o.improvement,
|
|
470
|
+
effort: 'medium',
|
|
471
|
+
sample_change: null,
|
|
472
|
+
})),
|
|
473
|
+
confidence: 0.85,
|
|
474
|
+
}));
|
|
475
|
+
const reliabilityProfiles = unified.inference_points.map(p => ({
|
|
476
|
+
inference_point_id: p.id,
|
|
477
|
+
line: p.line,
|
|
478
|
+
error_handling: {
|
|
479
|
+
has_try_catch: p.reliability_profile?.has_error_handling || false,
|
|
480
|
+
caught_exceptions: [],
|
|
481
|
+
specific_llm_errors: false,
|
|
482
|
+
error_logged: false,
|
|
483
|
+
error_propagated: false,
|
|
484
|
+
silent_failure: false,
|
|
485
|
+
user_friendly_error: false,
|
|
486
|
+
},
|
|
487
|
+
retry_strategy: {
|
|
488
|
+
has_retry: p.reliability_profile?.has_retry || false,
|
|
489
|
+
retry_library: 'none',
|
|
490
|
+
max_retries: null,
|
|
491
|
+
backoff_type: 'none',
|
|
492
|
+
initial_delay_ms: null,
|
|
493
|
+
max_delay_ms: null,
|
|
494
|
+
retry_on: [],
|
|
495
|
+
jitter: false,
|
|
496
|
+
retry_budget_risk: 'none',
|
|
497
|
+
},
|
|
498
|
+
fallback_strategy: {
|
|
499
|
+
has_fallback: p.reliability_profile?.has_fallback || false,
|
|
500
|
+
fallback_type: 'none',
|
|
501
|
+
fallback_model: null,
|
|
502
|
+
fallback_provider: null,
|
|
503
|
+
graceful_degradation: false,
|
|
504
|
+
fallback_tested: 'unknown',
|
|
505
|
+
},
|
|
506
|
+
timeout_handling: {
|
|
507
|
+
timeout_configured: p.reliability_profile?.has_timeout || false,
|
|
508
|
+
timeout_ms: null,
|
|
509
|
+
timeout_source: 'none',
|
|
510
|
+
on_timeout: 'none',
|
|
511
|
+
},
|
|
512
|
+
circuit_breaker: {
|
|
513
|
+
has_circuit_breaker: false,
|
|
514
|
+
library: null,
|
|
515
|
+
failure_threshold: null,
|
|
516
|
+
recovery_time_ms: null,
|
|
517
|
+
},
|
|
518
|
+
validation: {
|
|
519
|
+
validates_response: false,
|
|
520
|
+
validates_json: false,
|
|
521
|
+
validates_schema: false,
|
|
522
|
+
handles_empty_response: false,
|
|
523
|
+
handles_truncated: false,
|
|
524
|
+
},
|
|
525
|
+
reliability_risk: {
|
|
526
|
+
level: p.reliability_profile?.has_error_handling ? 'moderate' : 'fragile',
|
|
527
|
+
factors: [],
|
|
528
|
+
single_point_of_failure: !p.reliability_profile?.has_fallback,
|
|
529
|
+
cascade_risk: false,
|
|
530
|
+
data_loss_risk: false,
|
|
531
|
+
},
|
|
532
|
+
anti_patterns: (p.reliability_profile?.anti_patterns || []).map(a => ({
|
|
533
|
+
pattern: a.type,
|
|
534
|
+
description: a.description,
|
|
535
|
+
location: filePath,
|
|
536
|
+
severity: 'medium',
|
|
537
|
+
})),
|
|
538
|
+
optimizations: (p.reliability_profile?.optimizations || []).map(o => ({
|
|
539
|
+
type: o.type,
|
|
540
|
+
description: o.description,
|
|
541
|
+
reliability_before: 'low',
|
|
542
|
+
reliability_after: 'high',
|
|
543
|
+
effort: 'medium',
|
|
544
|
+
priority: (o.priority || 'medium'),
|
|
545
|
+
sample_change: null,
|
|
546
|
+
})),
|
|
547
|
+
confidence: 0.85,
|
|
548
|
+
}));
|
|
549
|
+
// Convert insights
|
|
550
|
+
const insights = (unified.insights || []).map((i, idx) => ({
|
|
551
|
+
id: `insight_${Date.now()}_${idx}`,
|
|
552
|
+
severity: i.severity,
|
|
553
|
+
category: i.category,
|
|
554
|
+
headline: i.headline,
|
|
555
|
+
evidence: i.evidence,
|
|
556
|
+
location: filePath,
|
|
557
|
+
recommendation: i.recommendation,
|
|
558
|
+
source: 'llm',
|
|
559
|
+
}));
|
|
560
|
+
return { imports, callsites, costProfiles, latencyProfiles, throughputProfiles, reliabilityProfiles, insights };
|
|
561
|
+
}
|
|
562
|
+
export class StaticAnalysisOrchestrator {
|
|
563
|
+
anthropicKey;
|
|
564
|
+
constructor(anthropicKey) {
|
|
565
|
+
// BYOK mode: user provides their own API key
|
|
566
|
+
this.anthropicKey = anthropicKey || process.env.ANTHROPIC_API_KEY || '';
|
|
567
|
+
if (!this.anthropicKey) {
|
|
568
|
+
throw new Error('ANTHROPIC_API_KEY is required. Set it via environment variable or pass it to the constructor.');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async analyze(input, onProgress) {
|
|
572
|
+
const allImports = [];
|
|
573
|
+
const allCallsites = [];
|
|
574
|
+
const allCostAnalysis = [];
|
|
575
|
+
const allLatencyAnalysis = [];
|
|
576
|
+
const allThroughputAnalysis = [];
|
|
577
|
+
const allReliabilityAnalysis = [];
|
|
578
|
+
const allPerformanceProfiles = [];
|
|
579
|
+
const allInsights = [];
|
|
580
|
+
// Track progress for Claude Code-style TUI
|
|
581
|
+
const totalFiles = input.files.length;
|
|
582
|
+
let completedFiles = 0;
|
|
583
|
+
// Emit initial progress
|
|
584
|
+
onProgress?.({
|
|
585
|
+
phase: 'analyzing',
|
|
586
|
+
completed: 0,
|
|
587
|
+
total: totalFiles,
|
|
588
|
+
percent: 0,
|
|
589
|
+
});
|
|
590
|
+
// Concurrent analysis with limited parallelism to avoid overwhelming system
|
|
591
|
+
// Max 20 concurrent API calls - balances speed vs resource usage
|
|
592
|
+
const MAX_CONCURRENT = 20;
|
|
593
|
+
const fileAnalyses = [];
|
|
594
|
+
// Process files in batches
|
|
595
|
+
for (let i = 0; i < input.files.length; i += MAX_CONCURRENT) {
|
|
596
|
+
const batch = input.files.slice(i, i + MAX_CONCURRENT);
|
|
597
|
+
const batchResults = await Promise.all(batch.map(async (file) => {
|
|
598
|
+
const language = file.language || detectLanguage(file.path);
|
|
599
|
+
const unified = await runUnifiedAnalysis(file.path, file.content, language, this.anthropicKey);
|
|
600
|
+
// Emit progress after each file completes (Claude Code pattern)
|
|
601
|
+
completedFiles++;
|
|
602
|
+
const percent = Math.round((completedFiles / totalFiles) * 100);
|
|
603
|
+
onProgress?.({
|
|
604
|
+
phase: 'analyzing',
|
|
605
|
+
completed: completedFiles,
|
|
606
|
+
total: totalFiles,
|
|
607
|
+
currentFile: file.path.split('/').pop() || file.path,
|
|
608
|
+
percent,
|
|
609
|
+
});
|
|
610
|
+
if (unified && unified.inference_points?.length > 0) {
|
|
611
|
+
return { file, unified, language };
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}));
|
|
615
|
+
fileAnalyses.push(...batchResults);
|
|
616
|
+
}
|
|
617
|
+
// Process results
|
|
618
|
+
for (const result of fileAnalyses) {
|
|
619
|
+
if (!result)
|
|
620
|
+
continue;
|
|
621
|
+
const { file, unified } = result;
|
|
622
|
+
const converted = convertUnifiedToLegacy(unified, file.path);
|
|
623
|
+
allImports.push(converted.imports);
|
|
624
|
+
allCallsites.push(converted.callsites);
|
|
625
|
+
allInsights.push(...converted.insights);
|
|
626
|
+
allCostAnalysis.push({
|
|
627
|
+
cost_profiles: converted.costProfiles,
|
|
628
|
+
summary: {
|
|
629
|
+
total_inference_points: converted.costProfiles.length,
|
|
630
|
+
estimated_cost_per_1k_calls: converted.costProfiles.reduce((sum, p) => sum + (p.cost_estimate?.per_call_typical || 0) * 1000, 0),
|
|
631
|
+
highest_cost_point: null,
|
|
632
|
+
optimization_potential_percent: 50,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
allLatencyAnalysis.push({
|
|
636
|
+
latency_profiles: converted.latencyProfiles,
|
|
637
|
+
summary: {
|
|
638
|
+
total_inference_points: converted.latencyProfiles.length,
|
|
639
|
+
blocking_calls: converted.latencyProfiles.filter(p => p.blocking_analysis?.is_blocking).length,
|
|
640
|
+
streaming_enabled: converted.latencyProfiles.filter(p => p.streaming_analysis?.streaming_enabled).length,
|
|
641
|
+
parallelizable: 0,
|
|
642
|
+
estimated_p95_ms: Math.max(...converted.latencyProfiles.map(p => p.latency_estimate?.p95_ms || 0), 5000),
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
allThroughputAnalysis.push({
|
|
646
|
+
throughput_profiles: converted.throughputProfiles,
|
|
647
|
+
summary: {
|
|
648
|
+
total_inference_points: converted.throughputProfiles.length,
|
|
649
|
+
has_rate_limiting: converted.throughputProfiles.filter(p => p.rate_limiting?.has_rate_limiter).length,
|
|
650
|
+
has_batching: converted.throughputProfiles.filter(p => p.batching_analysis?.batching_enabled).length,
|
|
651
|
+
scaling_bottlenecks: converted.throughputProfiles.reduce((sum, p) => sum + (p.scaling_analysis?.bottlenecks?.length || 0), 0),
|
|
652
|
+
estimated_max_rps: null,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
allReliabilityAnalysis.push({
|
|
656
|
+
reliability_profiles: converted.reliabilityProfiles,
|
|
657
|
+
summary: {
|
|
658
|
+
total_inference_points: converted.reliabilityProfiles.length,
|
|
659
|
+
has_error_handling: converted.reliabilityProfiles.filter(p => p.error_handling?.has_try_catch).length,
|
|
660
|
+
has_retry: converted.reliabilityProfiles.filter(p => p.retry_strategy?.has_retry).length,
|
|
661
|
+
has_fallback: converted.reliabilityProfiles.filter(p => p.fallback_strategy?.has_fallback).length,
|
|
662
|
+
anti_patterns_found: converted.reliabilityProfiles.reduce((sum, p) => sum + (p.anti_patterns?.length || 0), 0),
|
|
663
|
+
overall_reliability: 'moderate',
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
// Build performance profiles with issues from LLM
|
|
667
|
+
for (const point of converted.callsites.inference_points) {
|
|
668
|
+
// Find matching unified inference point to get issues
|
|
669
|
+
const unifiedPoint = unified.inference_points.find(up => up.id === point.id);
|
|
670
|
+
// Convert LLM-generated issues to Issue type
|
|
671
|
+
const issues = (unifiedPoint?.issues || []).map(issue => ({
|
|
672
|
+
type: issue.type,
|
|
673
|
+
severity: (issue.severity || 'warning'),
|
|
674
|
+
headline: issue.headline,
|
|
675
|
+
evidence: issue.evidence,
|
|
676
|
+
originalCode: issue.original_code || '',
|
|
677
|
+
suggestedFix: issue.suggested_fix || null,
|
|
678
|
+
aiAgentPrompt: issue.ai_agent_prompt || '',
|
|
679
|
+
}));
|
|
680
|
+
allPerformanceProfiles.push({
|
|
681
|
+
inference_point_id: point.id,
|
|
682
|
+
line: point.line,
|
|
683
|
+
file: file.path,
|
|
684
|
+
provider: point.provider.value,
|
|
685
|
+
model: point.model.value,
|
|
686
|
+
originalCode: unifiedPoint?.original_code || '',
|
|
687
|
+
issues,
|
|
688
|
+
cost: converted.costProfiles.find(p => p.inference_point_id === point.id) || null,
|
|
689
|
+
latency: converted.latencyProfiles.find(p => p.inference_point_id === point.id) || null,
|
|
690
|
+
throughput: converted.throughputProfiles.find(p => p.inference_point_id === point.id) || null,
|
|
691
|
+
reliability: converted.reliabilityProfiles.find(p => p.inference_point_id === point.id) || null,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Aggregate optimizations
|
|
696
|
+
const allOptimizations = [];
|
|
697
|
+
for (const profile of allPerformanceProfiles) {
|
|
698
|
+
if (profile.cost?.optimizations) {
|
|
699
|
+
for (const opt of profile.cost.optimizations) {
|
|
700
|
+
allOptimizations.push({
|
|
701
|
+
dimension: 'cost',
|
|
702
|
+
inference_point_id: profile.inference_point_id,
|
|
703
|
+
file: profile.file,
|
|
704
|
+
line: profile.line,
|
|
705
|
+
type: opt.type,
|
|
706
|
+
description: opt.description,
|
|
707
|
+
impact: `${opt.savings_percent}% savings`,
|
|
708
|
+
effort: opt.effort,
|
|
709
|
+
priority: opt.savings_percent > 50 ? 'high' : opt.savings_percent > 20 ? 'medium' : 'low',
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (profile.latency?.optimizations) {
|
|
714
|
+
for (const opt of profile.latency.optimizations) {
|
|
715
|
+
allOptimizations.push({
|
|
716
|
+
dimension: 'latency',
|
|
717
|
+
inference_point_id: profile.inference_point_id,
|
|
718
|
+
file: profile.file,
|
|
719
|
+
line: profile.line,
|
|
720
|
+
type: opt.type,
|
|
721
|
+
description: opt.description,
|
|
722
|
+
impact: `${opt.improvement_percent}% improvement`,
|
|
723
|
+
effort: opt.effort,
|
|
724
|
+
priority: opt.improvement_percent > 50 ? 'high' : 'medium',
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (profile.throughput?.optimizations) {
|
|
729
|
+
for (const opt of profile.throughput.optimizations) {
|
|
730
|
+
allOptimizations.push({
|
|
731
|
+
dimension: 'throughput',
|
|
732
|
+
inference_point_id: profile.inference_point_id,
|
|
733
|
+
file: profile.file,
|
|
734
|
+
line: profile.line,
|
|
735
|
+
type: opt.type,
|
|
736
|
+
description: opt.description,
|
|
737
|
+
impact: opt.improvement,
|
|
738
|
+
effort: opt.effort,
|
|
739
|
+
priority: opt.type === 'add_rate_limiter' ? 'high' : 'medium',
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (profile.reliability?.optimizations) {
|
|
744
|
+
for (const opt of profile.reliability.optimizations) {
|
|
745
|
+
allOptimizations.push({
|
|
746
|
+
dimension: 'reliability',
|
|
747
|
+
inference_point_id: profile.inference_point_id,
|
|
748
|
+
file: profile.file,
|
|
749
|
+
line: profile.line,
|
|
750
|
+
type: opt.type,
|
|
751
|
+
description: opt.description,
|
|
752
|
+
impact: `${opt.reliability_before} → ${opt.reliability_after}`,
|
|
753
|
+
effort: opt.effort,
|
|
754
|
+
priority: opt.priority,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// Calculate summary
|
|
760
|
+
const allProviders = new Set();
|
|
761
|
+
const allModels = new Set();
|
|
762
|
+
for (const callsite of allCallsites) {
|
|
763
|
+
callsite.summary.providers_detected.forEach(p => allProviders.add(p));
|
|
764
|
+
callsite.summary.models_detected.forEach(m => allModels.add(m));
|
|
765
|
+
}
|
|
766
|
+
const totalCostPer1k = allCostAnalysis.reduce((sum, a) => sum + a.summary.estimated_cost_per_1k_calls, 0);
|
|
767
|
+
const maxP95 = Math.max(...allLatencyAnalysis.map(a => a.summary.estimated_p95_ms), 0);
|
|
768
|
+
// Determine overall reliability
|
|
769
|
+
const reliabilityLevels = allReliabilityAnalysis.map(a => a.summary.overall_reliability);
|
|
770
|
+
const reliabilityScore = reliabilityLevels.reduce((sum, level) => {
|
|
771
|
+
switch (level) {
|
|
772
|
+
case 'resilient': return sum + 4;
|
|
773
|
+
case 'robust': return sum + 3;
|
|
774
|
+
case 'moderate': return sum + 2;
|
|
775
|
+
case 'fragile': return sum + 1;
|
|
776
|
+
default: return sum;
|
|
777
|
+
}
|
|
778
|
+
}, 0);
|
|
779
|
+
const avgReliability = reliabilityLevels.length > 0 ? reliabilityScore / reliabilityLevels.length : 1;
|
|
780
|
+
let overallReliability = 'fragile';
|
|
781
|
+
if (avgReliability >= 3.5)
|
|
782
|
+
overallReliability = 'resilient';
|
|
783
|
+
else if (avgReliability >= 2.5)
|
|
784
|
+
overallReliability = 'robust';
|
|
785
|
+
else if (avgReliability >= 1.5)
|
|
786
|
+
overallReliability = 'moderate';
|
|
787
|
+
return {
|
|
788
|
+
imports: allImports,
|
|
789
|
+
callsites: allCallsites,
|
|
790
|
+
performance_profiles: allPerformanceProfiles,
|
|
791
|
+
cost_analysis: allCostAnalysis,
|
|
792
|
+
latency_analysis: allLatencyAnalysis,
|
|
793
|
+
throughput_analysis: allThroughputAnalysis,
|
|
794
|
+
reliability_analysis: allReliabilityAnalysis,
|
|
795
|
+
summary: {
|
|
796
|
+
total_files: input.files.length,
|
|
797
|
+
total_inference_points: allPerformanceProfiles.length,
|
|
798
|
+
providers: [...allProviders],
|
|
799
|
+
models: [...allModels],
|
|
800
|
+
estimated_cost_per_1k_calls: totalCostPer1k,
|
|
801
|
+
cost_risk_high: allCostAnalysis.reduce((sum, a) => sum + a.cost_profiles.filter(p => p.cost_risk.level === 'high' || p.cost_risk.level === 'critical').length, 0),
|
|
802
|
+
blocking_calls: allLatencyAnalysis.reduce((sum, a) => sum + a.summary.blocking_calls, 0),
|
|
803
|
+
streaming_enabled: allLatencyAnalysis.reduce((sum, a) => sum + a.summary.streaming_enabled, 0),
|
|
804
|
+
estimated_p95_ms: maxP95,
|
|
805
|
+
has_rate_limiting: allThroughputAnalysis.reduce((sum, a) => sum + a.summary.has_rate_limiting, 0),
|
|
806
|
+
scaling_bottlenecks: allThroughputAnalysis.reduce((sum, a) => sum + a.summary.scaling_bottlenecks, 0),
|
|
807
|
+
has_error_handling: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.has_error_handling, 0),
|
|
808
|
+
has_retry: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.has_retry, 0),
|
|
809
|
+
has_fallback: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.has_fallback, 0),
|
|
810
|
+
anti_patterns_found: allReliabilityAnalysis.reduce((sum, a) => sum + a.summary.anti_patterns_found, 0),
|
|
811
|
+
overall_reliability: overallReliability,
|
|
812
|
+
total_optimizations: allOptimizations.length,
|
|
813
|
+
critical_optimizations: allOptimizations.filter(o => o.priority === 'critical' || o.priority === 'high').length,
|
|
814
|
+
},
|
|
815
|
+
all_optimizations: allOptimizations,
|
|
816
|
+
insights: allInsights,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// =============================================================================
|
|
821
|
+
// CONVENIENCE FUNCTION
|
|
822
|
+
// =============================================================================
|
|
823
|
+
export async function runStaticAnalysis(input, anthropicKey) {
|
|
824
|
+
const orchestrator = new StaticAnalysisOrchestrator(anthropicKey);
|
|
825
|
+
return orchestrator.analyze(input);
|
|
826
|
+
}
|
|
827
|
+
//# sourceMappingURL=orchestrator.js.map
|