@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,272 @@
1
+ /**
2
+ * What-If Command (v1.6 - GAP 5)
3
+ *
4
+ * CLI command for counterfactual analysis:
5
+ * - Model swap: What if we used a different model?
6
+ * - Streaming: What if we enabled/disabled streaming?
7
+ * - Batching: What if we batched requests?
8
+ * - Provider: What if we used a different provider?
9
+ *
10
+ * Leverages existing src/counterfactuals.ts module.
11
+ */
12
+
13
+ import { Command } from 'commander';
14
+ import { existsSync, readFileSync } from 'fs';
15
+ import { listRuns, loadRun } from '../history.js';
16
+ import {
17
+ generateCounterfactuals,
18
+ rankCounterfactuals,
19
+ formatCounterfactualSummary,
20
+ } from '../counterfactuals.js';
21
+ import type {
22
+ InferenceMap,
23
+ Counterfactual,
24
+ CounterfactualResult,
25
+ } from '../types.js';
26
+
27
+ // =============================================================================
28
+ // TYPES
29
+ // =============================================================================
30
+
31
+ interface WhatIfOptions {
32
+ model?: string;
33
+ provider?: string;
34
+ streaming?: string;
35
+ batchSize?: string;
36
+ run?: string;
37
+ priority?: 'latency' | 'cost' | 'balanced';
38
+ output?: string;
39
+ limit?: number;
40
+ }
41
+
42
+ // =============================================================================
43
+ // HELPERS
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Load inference map from history or file
48
+ */
49
+ function loadInferenceMap(runId?: string): InferenceMap | null {
50
+ if (runId) {
51
+ // Check if it's a file path
52
+ if (existsSync(runId) && runId.endsWith('.json')) {
53
+ try {
54
+ const content = readFileSync(runId, 'utf-8');
55
+ return JSON.parse(content);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ // Load from history
62
+ const run = loadRun(runId);
63
+ return run?.data?.inferenceMap || null;
64
+ }
65
+
66
+ // Load latest run
67
+ const runs = listRuns();
68
+ if (runs.length === 0) {
69
+ return null;
70
+ }
71
+
72
+ const latestRun = loadRun(runs[0].runId);
73
+ return latestRun?.data?.inferenceMap || null;
74
+ }
75
+
76
+ /**
77
+ * Filter counterfactuals by type/criteria
78
+ */
79
+ function filterCounterfactuals(
80
+ result: CounterfactualResult,
81
+ options: WhatIfOptions
82
+ ): Counterfactual[] {
83
+ let filtered = result.counterfactuals;
84
+
85
+ // Filter by type based on options
86
+ if (options.model) {
87
+ filtered = filtered.filter(c =>
88
+ c.type === 'model_swap' &&
89
+ c.proposedState.model?.toLowerCase().includes(options.model!.toLowerCase())
90
+ );
91
+ }
92
+
93
+ if (options.provider) {
94
+ filtered = filtered.filter(c =>
95
+ c.proposedState.provider?.toLowerCase().includes(options.provider!.toLowerCase())
96
+ );
97
+ }
98
+
99
+ if (options.streaming !== undefined) {
100
+ filtered = filtered.filter(c => c.type === 'streaming_enable');
101
+ }
102
+
103
+ if (options.batchSize) {
104
+ filtered = filtered.filter(c => c.type === 'batch_optimization');
105
+ }
106
+
107
+ return filtered;
108
+ }
109
+
110
+ /**
111
+ * Format counterfactual for display
112
+ */
113
+ function formatCounterfactual(cf: Counterfactual, index: number): string {
114
+ const lines: string[] = [];
115
+
116
+ // Header with number and headline
117
+ lines.push(`\n${index + 1}. ${cf.headline}`);
118
+ lines.push('─'.repeat(50));
119
+
120
+ // Type badge
121
+ const typeBadges: Record<string, string> = {
122
+ model_swap: 'Model Swap',
123
+ batch_optimization: 'Batching',
124
+ cache_addition: 'Caching',
125
+ streaming_enable: 'Streaming',
126
+ provider_change: 'Provider',
127
+ };
128
+ lines.push(` Type: ${typeBadges[cf.type] || cf.type}`);
129
+
130
+ // Current vs Proposed
131
+ lines.push('');
132
+ lines.push(' Current → Proposed:');
133
+ if (cf.currentState.model) {
134
+ lines.push(` Model: ${cf.currentState.model} → ${cf.proposedState.model || 'same'}`);
135
+ }
136
+ if (cf.currentState.provider || cf.proposedState.provider) {
137
+ lines.push(` Provider: ${cf.currentState.provider || '-'} → ${cf.proposedState.provider || 'same'}`);
138
+ }
139
+ if (cf.currentState.pattern || cf.proposedState.pattern) {
140
+ lines.push(` Pattern: ${cf.currentState.pattern || '-'} → ${cf.proposedState.pattern || 'same'}`);
141
+ }
142
+
143
+ // Impact
144
+ lines.push('');
145
+ lines.push(' Impact:');
146
+ if (cf.impact.latencyDeltaPercent !== 0) {
147
+ const sign = cf.impact.latencyDeltaPercent < 0 ? '' : '+';
148
+ lines.push(` Latency: ${sign}${cf.impact.latencyDeltaPercent}% (${sign}${cf.impact.latencyDelta}ms)`);
149
+ }
150
+ if (cf.impact.costDeltaPercent !== 0) {
151
+ const sign = cf.impact.costDeltaPercent < 0 ? '' : '+';
152
+ lines.push(` Cost: ${sign}${cf.impact.costDeltaPercent}%`);
153
+ }
154
+
155
+ // Effort and confidence
156
+ lines.push(` Effort: ${cf.effort} Confidence: ${cf.confidence}`);
157
+
158
+ // Tradeoffs
159
+ if (cf.impact.tradeoffs.length > 0) {
160
+ lines.push('');
161
+ lines.push(' Tradeoffs:');
162
+ for (const tradeoff of cf.impact.tradeoffs.slice(0, 3)) {
163
+ lines.push(` • ${tradeoff}`);
164
+ }
165
+ }
166
+
167
+ // Affected points
168
+ if (cf.affectedPoints.length > 0) {
169
+ lines.push(`\n Affects ${cf.affectedPoints.length} inference point${cf.affectedPoints.length !== 1 ? 's' : ''}`);
170
+ }
171
+
172
+ return lines.join('\n');
173
+ }
174
+
175
+ /**
176
+ * Display counterfactual results
177
+ */
178
+ function displayResults(counterfactuals: Counterfactual[], summary: CounterfactualResult['summary']): void {
179
+ if (counterfactuals.length === 0) {
180
+ console.log('\nNo optimization opportunities found matching your criteria.');
181
+ return;
182
+ }
183
+
184
+ console.log('\nWhat-If Analysis Results');
185
+ console.log('═'.repeat(50));
186
+ console.log(formatCounterfactualSummary({ counterfactuals, summary, generatedAt: new Date().toISOString() }));
187
+
188
+ for (let i = 0; i < counterfactuals.length; i++) {
189
+ console.log(formatCounterfactual(counterfactuals[i], i));
190
+ }
191
+
192
+ console.log('');
193
+ }
194
+
195
+ // =============================================================================
196
+ // COMMAND
197
+ // =============================================================================
198
+
199
+ /**
200
+ * Register what-if command
201
+ */
202
+ export function registerWhatIfCommand(program: Command): void {
203
+ program
204
+ .command('whatif')
205
+ .description('run counterfactual analysis ("what if" scenarios)')
206
+ .option('--model <model>', 'what if we used this model?')
207
+ .option('--provider <provider>', 'what if we used this provider?')
208
+ .option('--streaming <bool>', 'what if streaming was enabled/disabled?')
209
+ .option('--batch-size <n>', 'what if we batched requests?')
210
+ .option('--run <runId>', 'run ID or inference-map.json path (default: latest)')
211
+ .option('--priority <type>', 'ranking priority: latency, cost, balanced (default)', 'balanced')
212
+ .option('--output <format>', 'output format: text (default) or json', 'text')
213
+ .option('--limit <n>', 'limit number of results', parseInt)
214
+ .action(async (options: WhatIfOptions) => {
215
+ try {
216
+ // Load inference map
217
+ const inferenceMap = loadInferenceMap(options.run);
218
+
219
+ if (!inferenceMap) {
220
+ console.error('No analysis data found.');
221
+ console.log('\nRun "peakinfer analyze ." first, or specify:');
222
+ console.log(' --run <runId> Run ID from history');
223
+ console.log(' --run <file> Path to inference-map.json');
224
+ process.exit(1);
225
+ }
226
+
227
+ if (inferenceMap.callsites.length === 0) {
228
+ console.error('No inference points found in analysis.');
229
+ process.exit(1);
230
+ }
231
+
232
+ // Generate counterfactuals
233
+ const result = generateCounterfactuals(inferenceMap);
234
+
235
+ // Filter based on options
236
+ let counterfactuals = filterCounterfactuals(result, options);
237
+
238
+ // Rank by priority
239
+ if (options.priority && ['latency', 'cost', 'balanced'].includes(options.priority)) {
240
+ counterfactuals = rankCounterfactuals(
241
+ { ...result, counterfactuals },
242
+ options.priority as 'latency' | 'cost' | 'balanced'
243
+ );
244
+ }
245
+
246
+ // Apply limit
247
+ if (options.limit && options.limit > 0) {
248
+ counterfactuals = counterfactuals.slice(0, options.limit);
249
+ }
250
+
251
+ // Output
252
+ if (options.output === 'json') {
253
+ console.log(JSON.stringify({
254
+ counterfactuals,
255
+ summary: result.summary,
256
+ generatedAt: new Date().toISOString(),
257
+ }, null, 2));
258
+ } else {
259
+ displayResults(counterfactuals, result.summary);
260
+
261
+ // Show hints for more specific queries
262
+ if (!options.model && !options.provider && !options.streaming && !options.batchSize) {
263
+ console.log('Tip: Use --model, --provider, --streaming, or --batch-size to filter results.');
264
+ console.log('Example: peakinfer whatif --model gpt-4o-mini --priority cost');
265
+ }
266
+ }
267
+ } catch (error) {
268
+ console.error('Error:', error instanceof Error ? error.message : 'What-if analysis failed');
269
+ process.exit(1);
270
+ }
271
+ });
272
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Historical Comparison Module (v1.5)
3
+ *
4
+ * Compares current analysis with previous runs to surface:
5
+ * - New inference points (pre-deploy validation)
6
+ * - Removed inference points (cleanup validation)
7
+ * - Changed configurations (drift detection)
8
+ *
9
+ * Enables tracking changes over time for informed deployment decisions.
10
+ */
11
+
12
+ import type {
13
+ Callsite,
14
+ Insight,
15
+ ComparisonResult,
16
+ ChangedInferencePoint,
17
+ FieldChange,
18
+ HistoryManifest,
19
+ } from './types.js';
20
+
21
+ // =============================================================================
22
+ // TYPES
23
+ // =============================================================================
24
+
25
+ export interface CompareOptions {
26
+ /** Specific run ID to compare against (default: latest) */
27
+ baseRunId?: string;
28
+ /** Include insight comparison (default: true) */
29
+ compareInsights?: boolean;
30
+ }
31
+
32
+ export interface AnalysisSnapshot {
33
+ runId: string;
34
+ timestamp: string;
35
+ callsites: Callsite[];
36
+ insights?: Insight[];
37
+ }
38
+
39
+ // =============================================================================
40
+ // HELPERS
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Create a unique key for an inference point based on file:line.
45
+ * Used for matching inference points across runs.
46
+ */
47
+ function getCallsiteKey(callsite: Callsite): string {
48
+ return `${callsite.file}:${callsite.line}`;
49
+ }
50
+
51
+ /**
52
+ * Compare two values for equality (deep comparison for objects).
53
+ */
54
+ function valuesEqual(a: unknown, b: unknown): boolean {
55
+ if (a === b) return true;
56
+ if (a === null || b === null) return a === b;
57
+ if (typeof a !== typeof b) return false;
58
+ if (typeof a === 'object') {
59
+ return JSON.stringify(a) === JSON.stringify(b);
60
+ }
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * Find changes between two inference points.
66
+ */
67
+ function findCallsiteChanges(before: Callsite, after: Callsite): FieldChange[] {
68
+ const changes: FieldChange[] = [];
69
+ const fieldsToCompare: (keyof Callsite)[] = [
70
+ 'provider', 'model', 'framework', 'runtime', 'confidence'
71
+ ];
72
+
73
+ for (const field of fieldsToCompare) {
74
+ if (!valuesEqual(before[field], after[field])) {
75
+ changes.push({
76
+ field,
77
+ before: before[field],
78
+ after: after[field],
79
+ });
80
+ }
81
+ }
82
+
83
+ // Compare patterns object
84
+ const patternKeys = new Set([
85
+ ...Object.keys(before.patterns || {}),
86
+ ...Object.keys(after.patterns || {}),
87
+ ]);
88
+
89
+ for (const pattern of patternKeys) {
90
+ const beforeVal = (before.patterns as Record<string, unknown>)?.[pattern];
91
+ const afterVal = (after.patterns as Record<string, unknown>)?.[pattern];
92
+ if (!valuesEqual(beforeVal, afterVal)) {
93
+ changes.push({
94
+ field: `patterns.${pattern}`,
95
+ before: beforeVal,
96
+ after: afterVal,
97
+ });
98
+ }
99
+ }
100
+
101
+ return changes;
102
+ }
103
+
104
+ /**
105
+ * Compare insights between runs to find new/resolved issues.
106
+ */
107
+ function compareInsights(
108
+ baseInsights: Insight[],
109
+ currentInsights: Insight[]
110
+ ): { newCritical: number; resolvedCritical: number; newWarnings: number; resolvedWarnings: number } {
111
+ const baseIds = new Set(baseInsights.map(i => i.id || `${i.headline}:${i.location}`));
112
+ const currentIds = new Set(currentInsights.map(i => i.id || `${i.headline}:${i.location}`));
113
+
114
+ // New insights (in current but not in base)
115
+ const newInsights = currentInsights.filter(i => {
116
+ const id = i.id || `${i.headline}:${i.location}`;
117
+ return !baseIds.has(id);
118
+ });
119
+
120
+ // Resolved insights (in base but not in current)
121
+ const resolvedInsights = baseInsights.filter(i => {
122
+ const id = i.id || `${i.headline}:${i.location}`;
123
+ return !currentIds.has(id);
124
+ });
125
+
126
+ return {
127
+ newCritical: newInsights.filter(i => i.severity === 'critical').length,
128
+ resolvedCritical: resolvedInsights.filter(i => i.severity === 'critical').length,
129
+ newWarnings: newInsights.filter(i => i.severity === 'warning').length,
130
+ resolvedWarnings: resolvedInsights.filter(i => i.severity === 'warning').length,
131
+ };
132
+ }
133
+
134
+ // =============================================================================
135
+ // PUBLIC API
136
+ // =============================================================================
137
+
138
+ /**
139
+ * Compare two analysis snapshots.
140
+ */
141
+ export function compareSnapshots(
142
+ baseline: AnalysisSnapshot,
143
+ current: AnalysisSnapshot,
144
+ options: CompareOptions = {}
145
+ ): ComparisonResult {
146
+ const { compareInsights: shouldCompareInsights = true } = options;
147
+
148
+ // Build lookup maps
149
+ const baseMap = new Map<string, Callsite>();
150
+ for (const cs of baseline.callsites) {
151
+ baseMap.set(getCallsiteKey(cs), cs);
152
+ }
153
+
154
+ const currentMap = new Map<string, Callsite>();
155
+ for (const cs of current.callsites) {
156
+ currentMap.set(getCallsiteKey(cs), cs);
157
+ }
158
+
159
+ // Find added, removed, and changed inference points
160
+ const added: Callsite[] = [];
161
+ const removed: Callsite[] = [];
162
+ const changed: ChangedInferencePoint[] = [];
163
+
164
+ // Check current callsites against baseline
165
+ for (const [key, currentCs] of currentMap) {
166
+ const baseCs = baseMap.get(key);
167
+ if (!baseCs) {
168
+ // New inference point
169
+ added.push(currentCs);
170
+ } else {
171
+ // Check for changes
172
+ const changes = findCallsiteChanges(baseCs, currentCs);
173
+ if (changes.length > 0) {
174
+ changed.push({ point: currentCs, changes });
175
+ }
176
+ }
177
+ }
178
+
179
+ // Check for removed callsites
180
+ for (const [key, baseCs] of baseMap) {
181
+ if (!currentMap.has(key)) {
182
+ removed.push(baseCs);
183
+ }
184
+ }
185
+
186
+ // Build result
187
+ const result: ComparisonResult = {
188
+ baseRunId: baseline.runId,
189
+ baseTimestamp: baseline.timestamp,
190
+ currentRunId: current.runId,
191
+ currentTimestamp: current.timestamp,
192
+ added,
193
+ removed,
194
+ changed,
195
+ metrics: {
196
+ totalBefore: baseline.callsites.length,
197
+ totalAfter: current.callsites.length,
198
+ addedCount: added.length,
199
+ removedCount: removed.length,
200
+ changedCount: changed.length,
201
+ netChange: added.length - removed.length,
202
+ },
203
+ };
204
+
205
+ // Add insight deltas if requested
206
+ if (shouldCompareInsights && baseline.insights && current.insights) {
207
+ result.insightDeltas = compareInsights(baseline.insights, current.insights);
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ /**
214
+ * Format a comparison result as a human-readable summary.
215
+ * Provides concise, actionable summary for pre-deploy review.
216
+ */
217
+ export function formatComparisonSummary(comparison: ComparisonResult): string {
218
+ const lines: string[] = [];
219
+
220
+ // Header with delta
221
+ const delta = comparison.metrics.netChange;
222
+ const deltaStr = delta > 0 ? `+${delta}` : delta.toString();
223
+ lines.push(`Comparing with run from ${new Date(comparison.baseTimestamp).toLocaleDateString()}`);
224
+ lines.push(`Inference points: ${comparison.metrics.totalBefore} → ${comparison.metrics.totalAfter} (${deltaStr})`);
225
+ lines.push('');
226
+
227
+ // Changes summary
228
+ if (comparison.metrics.addedCount > 0) {
229
+ lines.push(` + ${comparison.metrics.addedCount} new inference point${comparison.metrics.addedCount !== 1 ? 's' : ''}`);
230
+ }
231
+ if (comparison.metrics.removedCount > 0) {
232
+ lines.push(` - ${comparison.metrics.removedCount} removed inference point${comparison.metrics.removedCount !== 1 ? 's' : ''}`);
233
+ }
234
+ if (comparison.metrics.changedCount > 0) {
235
+ lines.push(` ~ ${comparison.metrics.changedCount} modified inference point${comparison.metrics.changedCount !== 1 ? 's' : ''}`);
236
+ }
237
+
238
+ // Insight deltas (if available)
239
+ if (comparison.insightDeltas) {
240
+ const { newCritical, resolvedCritical, newWarnings, resolvedWarnings } = comparison.insightDeltas;
241
+ if (newCritical > 0) {
242
+ lines.push(` ! ${newCritical} new critical issue${newCritical !== 1 ? 's' : ''}`);
243
+ }
244
+ if (resolvedCritical > 0) {
245
+ lines.push(` [OK] ${resolvedCritical} critical issue${resolvedCritical !== 1 ? 's' : ''} resolved`);
246
+ }
247
+ if (newWarnings > 0) {
248
+ lines.push(` ! ${newWarnings} new warning${newWarnings !== 1 ? 's' : ''}`);
249
+ }
250
+ if (resolvedWarnings > 0) {
251
+ lines.push(` [OK] ${resolvedWarnings} warning${resolvedWarnings !== 1 ? 's' : ''} resolved`);
252
+ }
253
+ }
254
+
255
+ // No changes case
256
+ if (comparison.metrics.addedCount === 0 &&
257
+ comparison.metrics.removedCount === 0 &&
258
+ comparison.metrics.changedCount === 0) {
259
+ lines.push(' No changes detected');
260
+ }
261
+
262
+ return lines.join('\n');
263
+ }
264
+
265
+ /**
266
+ * Check if comparison has significant changes that warrant attention.
267
+ * Used to highlight important changes in the output.
268
+ */
269
+ export function hasSignificantChanges(comparison: ComparisonResult): boolean {
270
+ // Any added/removed/changed inference points are significant
271
+ if (comparison.metrics.addedCount > 0 ||
272
+ comparison.metrics.removedCount > 0 ||
273
+ comparison.metrics.changedCount > 0) {
274
+ return true;
275
+ }
276
+
277
+ // New critical insights are significant
278
+ if (comparison.insightDeltas?.newCritical && comparison.insightDeltas.newCritical > 0) {
279
+ return true;
280
+ }
281
+
282
+ return false;
283
+ }