@peakinfer/cli 1.0.133

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.env.example +6 -0
  3. package/.github/workflows/peakinfer.yml +64 -0
  4. package/CHANGELOG.md +31 -0
  5. package/LICENSE +190 -0
  6. package/README.md +335 -0
  7. package/data/inferencemax.json +274 -0
  8. package/dist/agent-analyzer.d.ts +45 -0
  9. package/dist/agent-analyzer.d.ts.map +1 -0
  10. package/dist/agent-analyzer.js +374 -0
  11. package/dist/agent-analyzer.js.map +1 -0
  12. package/dist/agent.d.ts +76 -0
  13. package/dist/agent.d.ts.map +1 -0
  14. package/dist/agent.js +965 -0
  15. package/dist/agent.js.map +1 -0
  16. package/dist/agents/correlation-analyzer.d.ts +34 -0
  17. package/dist/agents/correlation-analyzer.d.ts.map +1 -0
  18. package/dist/agents/correlation-analyzer.js +261 -0
  19. package/dist/agents/correlation-analyzer.js.map +1 -0
  20. package/dist/agents/index.d.ts +91 -0
  21. package/dist/agents/index.d.ts.map +1 -0
  22. package/dist/agents/index.js +111 -0
  23. package/dist/agents/index.js.map +1 -0
  24. package/dist/agents/runtime-analyzer.d.ts +38 -0
  25. package/dist/agents/runtime-analyzer.d.ts.map +1 -0
  26. package/dist/agents/runtime-analyzer.js +244 -0
  27. package/dist/agents/runtime-analyzer.js.map +1 -0
  28. package/dist/analysis-types.d.ts +500 -0
  29. package/dist/analysis-types.d.ts.map +1 -0
  30. package/dist/analysis-types.js +11 -0
  31. package/dist/analysis-types.js.map +1 -0
  32. package/dist/analytics.d.ts +25 -0
  33. package/dist/analytics.d.ts.map +1 -0
  34. package/dist/analytics.js +94 -0
  35. package/dist/analytics.js.map +1 -0
  36. package/dist/analyzer.d.ts +48 -0
  37. package/dist/analyzer.d.ts.map +1 -0
  38. package/dist/analyzer.js +547 -0
  39. package/dist/analyzer.js.map +1 -0
  40. package/dist/artifacts.d.ts +44 -0
  41. package/dist/artifacts.d.ts.map +1 -0
  42. package/dist/artifacts.js +165 -0
  43. package/dist/artifacts.js.map +1 -0
  44. package/dist/benchmarks/index.d.ts +88 -0
  45. package/dist/benchmarks/index.d.ts.map +1 -0
  46. package/dist/benchmarks/index.js +205 -0
  47. package/dist/benchmarks/index.js.map +1 -0
  48. package/dist/cli.d.ts +3 -0
  49. package/dist/cli.d.ts.map +1 -0
  50. package/dist/cli.js +427 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/commands/ci.d.ts +19 -0
  53. package/dist/commands/ci.d.ts.map +1 -0
  54. package/dist/commands/ci.js +253 -0
  55. package/dist/commands/ci.js.map +1 -0
  56. package/dist/commands/config.d.ts +16 -0
  57. package/dist/commands/config.d.ts.map +1 -0
  58. package/dist/commands/config.js +249 -0
  59. package/dist/commands/config.js.map +1 -0
  60. package/dist/commands/demo.d.ts +15 -0
  61. package/dist/commands/demo.d.ts.map +1 -0
  62. package/dist/commands/demo.js +106 -0
  63. package/dist/commands/demo.js.map +1 -0
  64. package/dist/commands/export.d.ts +14 -0
  65. package/dist/commands/export.d.ts.map +1 -0
  66. package/dist/commands/export.js +209 -0
  67. package/dist/commands/export.js.map +1 -0
  68. package/dist/commands/history.d.ts +15 -0
  69. package/dist/commands/history.d.ts.map +1 -0
  70. package/dist/commands/history.js +389 -0
  71. package/dist/commands/history.js.map +1 -0
  72. package/dist/commands/template.d.ts +14 -0
  73. package/dist/commands/template.d.ts.map +1 -0
  74. package/dist/commands/template.js +341 -0
  75. package/dist/commands/template.js.map +1 -0
  76. package/dist/commands/validate-map.d.ts +12 -0
  77. package/dist/commands/validate-map.d.ts.map +1 -0
  78. package/dist/commands/validate-map.js +274 -0
  79. package/dist/commands/validate-map.js.map +1 -0
  80. package/dist/commands/whatif.d.ts +17 -0
  81. package/dist/commands/whatif.d.ts.map +1 -0
  82. package/dist/commands/whatif.js +206 -0
  83. package/dist/commands/whatif.js.map +1 -0
  84. package/dist/comparison.d.ts +38 -0
  85. package/dist/comparison.d.ts.map +1 -0
  86. package/dist/comparison.js +223 -0
  87. package/dist/comparison.js.map +1 -0
  88. package/dist/config.d.ts +42 -0
  89. package/dist/config.d.ts.map +1 -0
  90. package/dist/config.js +158 -0
  91. package/dist/config.js.map +1 -0
  92. package/dist/connectors/helicone.d.ts +9 -0
  93. package/dist/connectors/helicone.d.ts.map +1 -0
  94. package/dist/connectors/helicone.js +106 -0
  95. package/dist/connectors/helicone.js.map +1 -0
  96. package/dist/connectors/index.d.ts +37 -0
  97. package/dist/connectors/index.d.ts.map +1 -0
  98. package/dist/connectors/index.js +65 -0
  99. package/dist/connectors/index.js.map +1 -0
  100. package/dist/connectors/langsmith.d.ts +9 -0
  101. package/dist/connectors/langsmith.d.ts.map +1 -0
  102. package/dist/connectors/langsmith.js +122 -0
  103. package/dist/connectors/langsmith.js.map +1 -0
  104. package/dist/connectors/types.d.ts +83 -0
  105. package/dist/connectors/types.d.ts.map +1 -0
  106. package/dist/connectors/types.js +98 -0
  107. package/dist/connectors/types.js.map +1 -0
  108. package/dist/cost-estimator.d.ts +46 -0
  109. package/dist/cost-estimator.d.ts.map +1 -0
  110. package/dist/cost-estimator.js +104 -0
  111. package/dist/cost-estimator.js.map +1 -0
  112. package/dist/costs.d.ts +57 -0
  113. package/dist/costs.d.ts.map +1 -0
  114. package/dist/costs.js +251 -0
  115. package/dist/costs.js.map +1 -0
  116. package/dist/counterfactuals.d.ts +29 -0
  117. package/dist/counterfactuals.d.ts.map +1 -0
  118. package/dist/counterfactuals.js +448 -0
  119. package/dist/counterfactuals.js.map +1 -0
  120. package/dist/enhancement-prompts.d.ts +41 -0
  121. package/dist/enhancement-prompts.d.ts.map +1 -0
  122. package/dist/enhancement-prompts.js +88 -0
  123. package/dist/enhancement-prompts.js.map +1 -0
  124. package/dist/envelopes.d.ts +20 -0
  125. package/dist/envelopes.d.ts.map +1 -0
  126. package/dist/envelopes.js +790 -0
  127. package/dist/envelopes.js.map +1 -0
  128. package/dist/format-normalizer.d.ts +71 -0
  129. package/dist/format-normalizer.d.ts.map +1 -0
  130. package/dist/format-normalizer.js +1331 -0
  131. package/dist/format-normalizer.js.map +1 -0
  132. package/dist/history.d.ts +79 -0
  133. package/dist/history.d.ts.map +1 -0
  134. package/dist/history.js +313 -0
  135. package/dist/history.js.map +1 -0
  136. package/dist/html.d.ts +11 -0
  137. package/dist/html.d.ts.map +1 -0
  138. package/dist/html.js +463 -0
  139. package/dist/html.js.map +1 -0
  140. package/dist/impact.d.ts +42 -0
  141. package/dist/impact.d.ts.map +1 -0
  142. package/dist/impact.js +443 -0
  143. package/dist/impact.js.map +1 -0
  144. package/dist/index.d.ts +26 -0
  145. package/dist/index.d.ts.map +1 -0
  146. package/dist/index.js +34 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/insights.d.ts +5 -0
  149. package/dist/insights.d.ts.map +1 -0
  150. package/dist/insights.js +271 -0
  151. package/dist/insights.js.map +1 -0
  152. package/dist/joiner.d.ts +9 -0
  153. package/dist/joiner.d.ts.map +1 -0
  154. package/dist/joiner.js +247 -0
  155. package/dist/joiner.js.map +1 -0
  156. package/dist/orchestrator.d.ts +34 -0
  157. package/dist/orchestrator.d.ts.map +1 -0
  158. package/dist/orchestrator.js +827 -0
  159. package/dist/orchestrator.js.map +1 -0
  160. package/dist/pdf.d.ts +26 -0
  161. package/dist/pdf.d.ts.map +1 -0
  162. package/dist/pdf.js +84 -0
  163. package/dist/pdf.js.map +1 -0
  164. package/dist/prediction.d.ts +33 -0
  165. package/dist/prediction.d.ts.map +1 -0
  166. package/dist/prediction.js +316 -0
  167. package/dist/prediction.js.map +1 -0
  168. package/dist/prompts/loader.d.ts +38 -0
  169. package/dist/prompts/loader.d.ts.map +1 -0
  170. package/dist/prompts/loader.js +60 -0
  171. package/dist/prompts/loader.js.map +1 -0
  172. package/dist/renderer.d.ts +64 -0
  173. package/dist/renderer.d.ts.map +1 -0
  174. package/dist/renderer.js +923 -0
  175. package/dist/renderer.js.map +1 -0
  176. package/dist/runid.d.ts +57 -0
  177. package/dist/runid.d.ts.map +1 -0
  178. package/dist/runid.js +199 -0
  179. package/dist/runid.js.map +1 -0
  180. package/dist/runtime.d.ts +29 -0
  181. package/dist/runtime.d.ts.map +1 -0
  182. package/dist/runtime.js +366 -0
  183. package/dist/runtime.js.map +1 -0
  184. package/dist/scanner.d.ts +11 -0
  185. package/dist/scanner.d.ts.map +1 -0
  186. package/dist/scanner.js +426 -0
  187. package/dist/scanner.js.map +1 -0
  188. package/dist/templates.d.ts +120 -0
  189. package/dist/templates.d.ts.map +1 -0
  190. package/dist/templates.js +429 -0
  191. package/dist/templates.js.map +1 -0
  192. package/dist/tools/index.d.ts +153 -0
  193. package/dist/tools/index.d.ts.map +1 -0
  194. package/dist/tools/index.js +177 -0
  195. package/dist/tools/index.js.map +1 -0
  196. package/dist/types.d.ts +3647 -0
  197. package/dist/types.d.ts.map +1 -0
  198. package/dist/types.js +703 -0
  199. package/dist/types.js.map +1 -0
  200. package/dist/version.d.ts +7 -0
  201. package/dist/version.d.ts.map +1 -0
  202. package/dist/version.js +23 -0
  203. package/dist/version.js.map +1 -0
  204. package/docs/demo-guide.md +423 -0
  205. package/docs/events-format.md +295 -0
  206. package/docs/inferencemap-spec.md +344 -0
  207. package/docs/migration-v2.md +293 -0
  208. package/fixtures/demo/precomputed.json +142 -0
  209. package/fixtures/demo-project/README.md +52 -0
  210. package/fixtures/demo-project/ai-service.ts +65 -0
  211. package/fixtures/demo-project/sample-events.jsonl +15 -0
  212. package/fixtures/demo-project/src/ai-service.ts +128 -0
  213. package/fixtures/demo-project/src/llm-client.ts +155 -0
  214. package/package.json +65 -0
  215. package/prompts/agent-analyzer.yaml +47 -0
  216. package/prompts/ci-gate.yaml +98 -0
  217. package/prompts/correlation-analyzer.yaml +178 -0
  218. package/prompts/format-normalizer.yaml +46 -0
  219. package/prompts/peak-performance.yaml +180 -0
  220. package/prompts/pr-comment.yaml +111 -0
  221. package/prompts/runtime-analyzer.yaml +189 -0
  222. package/prompts/unified-analyzer.yaml +241 -0
  223. package/schemas/inference-map.v0.1.json +215 -0
  224. package/scripts/benchmark.ts +394 -0
  225. package/scripts/demo-v1.5.sh +158 -0
  226. package/scripts/sync-from-site.sh +197 -0
  227. package/scripts/validate-sync.sh +178 -0
  228. package/src/agent-analyzer.ts +481 -0
  229. package/src/agent.ts +1232 -0
  230. package/src/agents/correlation-analyzer.ts +353 -0
  231. package/src/agents/index.ts +235 -0
  232. package/src/agents/runtime-analyzer.ts +343 -0
  233. package/src/analysis-types.ts +558 -0
  234. package/src/analytics.ts +100 -0
  235. package/src/analyzer.ts +692 -0
  236. package/src/artifacts.ts +218 -0
  237. package/src/benchmarks/index.ts +309 -0
  238. package/src/cli.ts +503 -0
  239. package/src/commands/ci.ts +336 -0
  240. package/src/commands/config.ts +288 -0
  241. package/src/commands/demo.ts +175 -0
  242. package/src/commands/export.ts +297 -0
  243. package/src/commands/history.ts +425 -0
  244. package/src/commands/template.ts +385 -0
  245. package/src/commands/validate-map.ts +324 -0
  246. package/src/commands/whatif.ts +272 -0
  247. package/src/comparison.ts +283 -0
  248. package/src/config.ts +188 -0
  249. package/src/connectors/helicone.ts +164 -0
  250. package/src/connectors/index.ts +93 -0
  251. package/src/connectors/langsmith.ts +179 -0
  252. package/src/connectors/types.ts +180 -0
  253. package/src/cost-estimator.ts +146 -0
  254. package/src/costs.ts +347 -0
  255. package/src/counterfactuals.ts +516 -0
  256. package/src/enhancement-prompts.ts +118 -0
  257. package/src/envelopes.ts +814 -0
  258. package/src/format-normalizer.ts +1486 -0
  259. package/src/history.ts +400 -0
  260. package/src/html.ts +512 -0
  261. package/src/impact.ts +522 -0
  262. package/src/index.ts +83 -0
  263. package/src/insights.ts +341 -0
  264. package/src/joiner.ts +289 -0
  265. package/src/orchestrator.ts +1015 -0
  266. package/src/pdf.ts +110 -0
  267. package/src/prediction.ts +392 -0
  268. package/src/prompts/loader.ts +88 -0
  269. package/src/renderer.ts +1045 -0
  270. package/src/runid.ts +261 -0
  271. package/src/runtime.ts +450 -0
  272. package/src/scanner.ts +508 -0
  273. package/src/templates.ts +561 -0
  274. package/src/tools/index.ts +214 -0
  275. package/src/types.ts +873 -0
  276. package/src/version.ts +24 -0
  277. package/templates/context-accumulation.yaml +23 -0
  278. package/templates/cost-concentration.yaml +20 -0
  279. package/templates/dead-code.yaml +20 -0
  280. package/templates/latency-explainer.yaml +23 -0
  281. package/templates/optimizations/ab-testing-framework.yaml +74 -0
  282. package/templates/optimizations/api-gateway-optimization.yaml +81 -0
  283. package/templates/optimizations/api-model-routing-strategy.yaml +126 -0
  284. package/templates/optimizations/auto-scaling-optimization.yaml +85 -0
  285. package/templates/optimizations/batch-utilization-diagnostic.yaml +142 -0
  286. package/templates/optimizations/comprehensive-apm.yaml +76 -0
  287. package/templates/optimizations/context-window-optimization.yaml +91 -0
  288. package/templates/optimizations/cost-sensitive-batch-processing.yaml +77 -0
  289. package/templates/optimizations/distributed-training-optimization.yaml +77 -0
  290. package/templates/optimizations/document-analysis-edge.yaml +77 -0
  291. package/templates/optimizations/document-pipeline-optimization.yaml +78 -0
  292. package/templates/optimizations/domain-specific-distillation.yaml +78 -0
  293. package/templates/optimizations/error-handling-optimization.yaml +76 -0
  294. package/templates/optimizations/gptq-4bit-quantization.yaml +96 -0
  295. package/templates/optimizations/long-context-memory-management.yaml +78 -0
  296. package/templates/optimizations/max-tokens-optimization.yaml +76 -0
  297. package/templates/optimizations/memory-bandwidth-optimization.yaml +73 -0
  298. package/templates/optimizations/multi-framework-resilience.yaml +75 -0
  299. package/templates/optimizations/multi-tenant-optimization.yaml +75 -0
  300. package/templates/optimizations/prompt-caching-optimization.yaml +143 -0
  301. package/templates/optimizations/pytorch-to-onnx-migration.yaml +109 -0
  302. package/templates/optimizations/quality-monitoring.yaml +74 -0
  303. package/templates/optimizations/realtime-budget-controls.yaml +74 -0
  304. package/templates/optimizations/realtime-latency-optimization.yaml +74 -0
  305. package/templates/optimizations/sglang-concurrency-optimization.yaml +78 -0
  306. package/templates/optimizations/smart-model-routing.yaml +96 -0
  307. package/templates/optimizations/streaming-batch-selection.yaml +167 -0
  308. package/templates/optimizations/system-prompt-optimization.yaml +75 -0
  309. package/templates/optimizations/tensorrt-llm-performance.yaml +77 -0
  310. package/templates/optimizations/vllm-high-throughput-optimization.yaml +93 -0
  311. package/templates/optimizations/vllm-migration-memory-bound.yaml +78 -0
  312. package/templates/overpowered-extraction.yaml +32 -0
  313. package/templates/overpowered-model.yaml +31 -0
  314. package/templates/prompt-bloat.yaml +24 -0
  315. package/templates/retry-explosion.yaml +28 -0
  316. package/templates/schema/insight.schema.json +113 -0
  317. package/templates/schema/optimization.schema.json +180 -0
  318. package/templates/streaming-drift.yaml +30 -0
  319. package/templates/throughput-gap.yaml +21 -0
  320. package/templates/token-underutilization.yaml +28 -0
  321. package/templates/untested-fallback.yaml +21 -0
  322. package/tests/accuracy/drift-detection.test.ts +184 -0
  323. package/tests/accuracy/false-positives.test.ts +166 -0
  324. package/tests/accuracy/templates.test.ts +205 -0
  325. package/tests/action/commands.test.ts +125 -0
  326. package/tests/action/comments.test.ts +347 -0
  327. package/tests/cli.test.ts +203 -0
  328. package/tests/comparison.test.ts +309 -0
  329. package/tests/correlation-analyzer.test.ts +534 -0
  330. package/tests/counterfactuals.test.ts +347 -0
  331. package/tests/fixtures/events/missing-id.jsonl +1 -0
  332. package/tests/fixtures/events/missing-input.jsonl +1 -0
  333. package/tests/fixtures/events/missing-latency.jsonl +1 -0
  334. package/tests/fixtures/events/missing-model.jsonl +1 -0
  335. package/tests/fixtures/events/missing-output.jsonl +1 -0
  336. package/tests/fixtures/events/missing-provider.jsonl +1 -0
  337. package/tests/fixtures/events/missing-ts.jsonl +1 -0
  338. package/tests/fixtures/events/valid.csv +3 -0
  339. package/tests/fixtures/events/valid.json +1 -0
  340. package/tests/fixtures/events/valid.jsonl +2 -0
  341. package/tests/fixtures/events/with-callsite.jsonl +1 -0
  342. package/tests/fixtures/events/with-intent.jsonl +1 -0
  343. package/tests/fixtures/events/wrong-type.jsonl +1 -0
  344. package/tests/fixtures/repos/empty/.gitkeep +0 -0
  345. package/tests/fixtures/repos/hybrid-router/router.py +35 -0
  346. package/tests/fixtures/repos/saas-anthropic/agent.ts +27 -0
  347. package/tests/fixtures/repos/saas-openai/assistant.js +33 -0
  348. package/tests/fixtures/repos/saas-openai/client.py +26 -0
  349. package/tests/fixtures/repos/self-hosted-vllm/inference.py +22 -0
  350. package/tests/github-action.test.ts +292 -0
  351. package/tests/insights.test.ts +878 -0
  352. package/tests/joiner.test.ts +168 -0
  353. package/tests/performance/action-latency.test.ts +132 -0
  354. package/tests/performance/benchmark.test.ts +189 -0
  355. package/tests/performance/cli-latency.test.ts +102 -0
  356. package/tests/pr-comment.test.ts +313 -0
  357. package/tests/prediction.test.ts +296 -0
  358. package/tests/runtime-analyzer.test.ts +375 -0
  359. package/tests/runtime.test.ts +205 -0
  360. package/tests/scanner.test.ts +122 -0
  361. package/tests/template-conformance.test.ts +526 -0
  362. package/tests/unit/cost-calculator.test.ts +303 -0
  363. package/tests/unit/credits.test.ts +180 -0
  364. package/tests/unit/inference-map.test.ts +276 -0
  365. package/tests/unit/schema.test.ts +300 -0
  366. package/tsconfig.json +20 -0
  367. package/vitest.config.ts +14 -0
package/src/history.ts ADDED
@@ -0,0 +1,400 @@
1
+ /**
2
+ * History Storage Module (v1.5)
3
+ *
4
+ * Enables persistent storage of analysis runs for:
5
+ * - Historical comparison (Feature 2)
6
+ * - Deploy-time prediction (Feature 3)
7
+ *
8
+ * Directory structure:
9
+ * .peakinfer/
10
+ * └── history/
11
+ * ├── index.json # Global index of all runs
12
+ * └── <runId>/ # Individual run storage
13
+ * ├── manifest.json
14
+ * ├── inference-map.json
15
+ * └── analysis.json
16
+ */
17
+
18
+ import { createHash, randomUUID } from 'crypto';
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'fs';
20
+ import { join, resolve } from 'path';
21
+ import type {
22
+ HistoryManifest,
23
+ HistoryIndex,
24
+ AnalysisType,
25
+ InferenceMap,
26
+ Insight,
27
+ JoinedOutput,
28
+ RuntimeSummary,
29
+ } from './types.js';
30
+ import { VERSION } from './version.js';
31
+
32
+ // =============================================================================
33
+ // CONSTANTS
34
+ // =============================================================================
35
+
36
+ const HISTORY_DIR = '.peakinfer/history';
37
+ const INDEX_FILE = 'index.json';
38
+ const HISTORY_VERSION = '1.0';
39
+
40
+ // =============================================================================
41
+ // TYPES
42
+ // =============================================================================
43
+
44
+ export interface AnalysisData {
45
+ inferenceMap?: InferenceMap;
46
+ insights?: Insight[];
47
+ joined?: JoinedOutput;
48
+ runtime?: RuntimeSummary;
49
+ }
50
+
51
+ export interface SaveRunOptions {
52
+ path: string;
53
+ analysisType: AnalysisType;
54
+ data: AnalysisData;
55
+ durationMs?: number;
56
+ htmlPath?: string;
57
+ pdfPath?: string;
58
+ }
59
+
60
+ export interface LoadedRun {
61
+ manifest: HistoryManifest;
62
+ data: AnalysisData;
63
+ }
64
+
65
+ // =============================================================================
66
+ // HELPERS
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Get the history directory path (relative to cwd or specified base)
71
+ */
72
+ export function getHistoryDir(baseDir: string = '.'): string {
73
+ return join(baseDir, HISTORY_DIR);
74
+ }
75
+
76
+ /**
77
+ * Create a deterministic hash from a normalized path.
78
+ * Used for efficient lookup of runs for a specific project.
79
+ */
80
+ export function hashPath(path: string): string {
81
+ const normalized = resolve(path).toLowerCase();
82
+ return createHash('sha256').update(normalized).digest('hex').slice(0, 12);
83
+ }
84
+
85
+ /**
86
+ * Generate a unique run ID (timestamp-based for chronological ordering)
87
+ */
88
+ function generateHistoryRunId(): string {
89
+ const timestamp = Date.now().toString(36); // Base36 timestamp
90
+ const random = randomUUID().slice(0, 8); // Short random suffix
91
+ return `${timestamp}-${random}`;
92
+ }
93
+
94
+ /**
95
+ * Ensure directory exists
96
+ */
97
+ function ensureDir(dir: string): void {
98
+ if (!existsSync(dir)) {
99
+ mkdirSync(dir, { recursive: true });
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Write JSON file with pretty printing
105
+ */
106
+ function writeJSON(filePath: string, data: unknown): void {
107
+ writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
108
+ }
109
+
110
+ /**
111
+ * Read JSON file safely
112
+ */
113
+ function readJSON<T>(filePath: string): T | null {
114
+ try {
115
+ if (!existsSync(filePath)) return null;
116
+ const content = readFileSync(filePath, 'utf-8');
117
+ return JSON.parse(content) as T;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ // =============================================================================
124
+ // INDEX MANAGEMENT
125
+ // =============================================================================
126
+
127
+ /**
128
+ * Load the history index
129
+ */
130
+ function loadIndex(historyDir: string): HistoryIndex {
131
+ const indexPath = join(historyDir, INDEX_FILE);
132
+ const existing = readJSON<HistoryIndex>(indexPath);
133
+
134
+ if (existing) {
135
+ return existing;
136
+ }
137
+
138
+ // Return empty index
139
+ return {
140
+ version: HISTORY_VERSION,
141
+ lastUpdated: new Date().toISOString(),
142
+ runs: [],
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Save the history index
148
+ */
149
+ function saveIndex(historyDir: string, index: HistoryIndex): void {
150
+ ensureDir(historyDir);
151
+ const indexPath = join(historyDir, INDEX_FILE);
152
+ index.lastUpdated = new Date().toISOString();
153
+ writeJSON(indexPath, index);
154
+ }
155
+
156
+ // =============================================================================
157
+ // PUBLIC API
158
+ // =============================================================================
159
+
160
+ /**
161
+ * Save an analysis run to history.
162
+ * Returns the run ID for reference.
163
+ */
164
+ export function saveRun(options: SaveRunOptions, baseDir: string = '.'): string {
165
+ const historyDir = getHistoryDir(baseDir);
166
+ const runId = generateHistoryRunId();
167
+ const runDir = join(historyDir, runId);
168
+ const pathHash = hashPath(options.path);
169
+
170
+ ensureDir(runDir);
171
+
172
+ // Calculate summary metrics
173
+ const inferencePointCount = options.data.inferenceMap?.callsites?.length ?? 0;
174
+ const eventCount = options.data.runtime?.totalEvents;
175
+ const driftCount = options.data.joined?.drift?.length;
176
+ const insightCount = options.data.insights?.length;
177
+
178
+ // Track saved artifacts
179
+ const artifacts: HistoryManifest['artifacts'] = {};
180
+
181
+ // Save inference map
182
+ if (options.data.inferenceMap) {
183
+ const fileName = 'inference-map.json';
184
+ writeJSON(join(runDir, fileName), options.data.inferenceMap);
185
+ artifacts.inferenceMap = fileName;
186
+ }
187
+
188
+ // Save full analysis data
189
+ const analysisFileName = 'analysis.json';
190
+ writeJSON(join(runDir, analysisFileName), options.data);
191
+ artifacts.analysis = analysisFileName;
192
+
193
+ // Record report paths if provided
194
+ if (options.htmlPath) {
195
+ artifacts.html = options.htmlPath;
196
+ }
197
+ if (options.pdfPath) {
198
+ artifacts.pdf = options.pdfPath;
199
+ }
200
+
201
+ // Create manifest
202
+ const manifest: HistoryManifest = {
203
+ runId,
204
+ timestamp: new Date().toISOString(),
205
+ path: resolve(options.path),
206
+ pathHash,
207
+ analysisType: options.analysisType,
208
+ version: VERSION,
209
+ inferencePointCount,
210
+ eventCount,
211
+ driftCount,
212
+ insightCount,
213
+ durationMs: options.durationMs,
214
+ artifacts,
215
+ };
216
+
217
+ // Save manifest
218
+ writeJSON(join(runDir, 'manifest.json'), manifest);
219
+
220
+ // Update index
221
+ const index = loadIndex(historyDir);
222
+ index.runs.push({
223
+ runId,
224
+ timestamp: manifest.timestamp,
225
+ pathHash,
226
+ analysisType: options.analysisType,
227
+ inferencePointCount,
228
+ });
229
+ saveIndex(historyDir, index);
230
+
231
+ return runId;
232
+ }
233
+
234
+ /**
235
+ * Load a specific run by ID.
236
+ */
237
+ export function loadRun(runId: string, baseDir: string = '.'): LoadedRun | null {
238
+ const historyDir = getHistoryDir(baseDir);
239
+ const runDir = join(historyDir, runId);
240
+
241
+ // Load manifest
242
+ const manifest = readJSON<HistoryManifest>(join(runDir, 'manifest.json'));
243
+ if (!manifest) {
244
+ return null;
245
+ }
246
+
247
+ // Load analysis data
248
+ const data = readJSON<AnalysisData>(join(runDir, 'analysis.json')) ?? {};
249
+
250
+ return { manifest, data };
251
+ }
252
+
253
+ /**
254
+ * List all runs for a specific path (or all runs if no path specified).
255
+ * Returns runs sorted by timestamp (most recent first).
256
+ */
257
+ export function listRuns(path?: string, baseDir: string = '.'): HistoryManifest[] {
258
+ const historyDir = getHistoryDir(baseDir);
259
+ const index = loadIndex(historyDir);
260
+
261
+ // Filter by path hash if specified
262
+ const pathHash = path ? hashPath(path) : null;
263
+ const filteredRuns = pathHash
264
+ ? index.runs.filter(r => r.pathHash === pathHash)
265
+ : index.runs;
266
+
267
+ // Load full manifests for filtered runs
268
+ const manifests: HistoryManifest[] = [];
269
+ for (const run of filteredRuns) {
270
+ const runDir = join(historyDir, run.runId);
271
+ const manifest = readJSON<HistoryManifest>(join(runDir, 'manifest.json'));
272
+ if (manifest) {
273
+ manifests.push(manifest);
274
+ }
275
+ }
276
+
277
+ // Sort by timestamp descending (most recent first)
278
+ manifests.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
279
+
280
+ return manifests;
281
+ }
282
+
283
+ /**
284
+ * Get the most recent run for a path.
285
+ * Returns null if no history exists.
286
+ */
287
+ export function getLatestRun(path: string, baseDir: string = '.'): LoadedRun | null {
288
+ const runs = listRuns(path, baseDir);
289
+
290
+ if (runs.length === 0) {
291
+ return null;
292
+ }
293
+
294
+ // First run is most recent (already sorted)
295
+ return loadRun(runs[0].runId, baseDir);
296
+ }
297
+
298
+ /**
299
+ * Prune old runs, keeping only the most recent N runs per path.
300
+ * Returns the number of runs deleted.
301
+ */
302
+ export function pruneHistory(keepCount: number = 10, baseDir: string = '.'): number {
303
+ const historyDir = getHistoryDir(baseDir);
304
+ const index = loadIndex(historyDir);
305
+
306
+ // Group runs by pathHash
307
+ const runsByPath = new Map<string, typeof index.runs>();
308
+ for (const run of index.runs) {
309
+ const existing = runsByPath.get(run.pathHash) ?? [];
310
+ existing.push(run);
311
+ runsByPath.set(run.pathHash, existing);
312
+ }
313
+
314
+ // Find runs to delete
315
+ const runsToDelete: string[] = [];
316
+ for (const [, runs] of runsByPath) {
317
+ // Sort by timestamp descending
318
+ runs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
319
+
320
+ // Mark excess runs for deletion
321
+ if (runs.length > keepCount) {
322
+ for (let i = keepCount; i < runs.length; i++) {
323
+ runsToDelete.push(runs[i].runId);
324
+ }
325
+ }
326
+ }
327
+
328
+ // Delete run directories
329
+ for (const runId of runsToDelete) {
330
+ const runDir = join(historyDir, runId);
331
+ if (existsSync(runDir)) {
332
+ rmSync(runDir, { recursive: true });
333
+ }
334
+ }
335
+
336
+ // Update index to remove deleted runs
337
+ if (runsToDelete.length > 0) {
338
+ const deleteSet = new Set(runsToDelete);
339
+ index.runs = index.runs.filter(r => !deleteSet.has(r.runId));
340
+ saveIndex(historyDir, index);
341
+ }
342
+
343
+ return runsToDelete.length;
344
+ }
345
+
346
+ /**
347
+ * Delete a specific run by ID.
348
+ * Returns true if the run was deleted, false if not found.
349
+ */
350
+ export function deleteRun(runId: string, baseDir: string = '.'): boolean {
351
+ const historyDir = getHistoryDir(baseDir);
352
+ const runDir = join(historyDir, runId);
353
+
354
+ // Check if run exists
355
+ if (!existsSync(runDir)) {
356
+ return false;
357
+ }
358
+
359
+ // Delete the run directory
360
+ rmSync(runDir, { recursive: true });
361
+
362
+ // Update the index
363
+ const index = loadIndex(historyDir);
364
+ const originalLength = index.runs.length;
365
+ index.runs = index.runs.filter(r => r.runId !== runId);
366
+
367
+ if (index.runs.length < originalLength) {
368
+ saveIndex(historyDir, index);
369
+ }
370
+
371
+ return true;
372
+ }
373
+
374
+ /**
375
+ * Clear all history (delete everything).
376
+ * Returns the number of runs deleted.
377
+ */
378
+ export function clearAllHistory(baseDir: string = '.'): number {
379
+ const historyDir = getHistoryDir(baseDir);
380
+ const index = loadIndex(historyDir);
381
+ const count = index.runs.length;
382
+
383
+ // Delete all run directories
384
+ for (const run of index.runs) {
385
+ const runDir = join(historyDir, run.runId);
386
+ if (existsSync(runDir)) {
387
+ rmSync(runDir, { recursive: true });
388
+ }
389
+ }
390
+
391
+ // Reset the index
392
+ const emptyIndex: HistoryIndex = {
393
+ version: HISTORY_VERSION,
394
+ lastUpdated: new Date().toISOString(),
395
+ runs: [],
396
+ };
397
+ saveIndex(historyDir, emptyIndex);
398
+
399
+ return count;
400
+ }