@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/runid.ts ADDED
@@ -0,0 +1,261 @@
1
+ import { createHash } from 'crypto';
2
+ import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import type { InferenceMap, Insight, JoinedOutput, RuntimeSummary } from './types.js';
5
+ import { VERSION } from './version.js';
6
+
7
+ // =============================================================================
8
+ // TYPES
9
+ // =============================================================================
10
+
11
+ export interface RunInputs {
12
+ repoRoot?: string;
13
+ eventsPath?: string;
14
+ offline?: boolean;
15
+ }
16
+
17
+ export interface RunManifest {
18
+ runId: string;
19
+ version: string;
20
+ createdAt: string;
21
+ inputs: {
22
+ repoRoot?: string;
23
+ repoHash?: string;
24
+ eventsPath?: string;
25
+ eventsHash?: string;
26
+ offline: boolean;
27
+ };
28
+ artifacts: string[];
29
+ status: 'complete' | 'partial' | 'failed';
30
+ }
31
+
32
+ export interface CachedArtifacts {
33
+ inferenceMap?: InferenceMap;
34
+ insights?: Insight[];
35
+ joined?: JoinedOutput;
36
+ runtime?: RuntimeSummary;
37
+ }
38
+
39
+ // =============================================================================
40
+ // HELPERS
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Hash a file's content for change detection
45
+ */
46
+ function hashFile(filePath: string): string {
47
+ try {
48
+ const content = readFileSync(filePath, 'utf-8');
49
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
50
+ } catch {
51
+ return 'missing';
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Hash a directory structure (file paths + sizes + mtimes)
57
+ * This is fast and detects most changes without reading all file contents
58
+ */
59
+ function hashDirectory(dirPath: string, maxDepth: number = 5): string {
60
+ const hash = createHash('sha256');
61
+
62
+ function walk(dir: string, depth: number): void {
63
+ if (depth > maxDepth) return;
64
+
65
+ try {
66
+ const entries = readdirSync(dir, { withFileTypes: true });
67
+ entries.sort((a, b) => a.name.localeCompare(b.name));
68
+
69
+ for (const entry of entries) {
70
+ const fullPath = join(dir, entry.name);
71
+
72
+ // Skip common non-code directories
73
+ if (entry.isDirectory()) {
74
+ if (['node_modules', 'dist', '.git', '__pycache__', '.peakinfer', '.venv', 'venv'].includes(entry.name)) {
75
+ continue;
76
+ }
77
+ walk(fullPath, depth + 1);
78
+ } else {
79
+ // Only hash source files
80
+ if (/\.(ts|tsx|js|jsx|py|go|java|rs|rb|php|cs)$/.test(entry.name)) {
81
+ try {
82
+ const stat = statSync(fullPath);
83
+ hash.update(`${fullPath}:${stat.size}:${stat.mtimeMs}`);
84
+ } catch {
85
+ // Skip files we can't stat
86
+ }
87
+ }
88
+ }
89
+ }
90
+ } catch {
91
+ // Skip directories we can't read
92
+ }
93
+ }
94
+
95
+ walk(dirPath, 0);
96
+ return hash.digest('hex').slice(0, 16);
97
+ }
98
+
99
+ // =============================================================================
100
+ // PUBLIC API
101
+ // =============================================================================
102
+
103
+ /**
104
+ * Generate a deterministic run ID based on inputs
105
+ *
106
+ * runId = hash(version, repoHash?, eventsHash?, offline)
107
+ *
108
+ * This ensures:
109
+ * - Same inputs = same runId = can resume
110
+ * - Changed inputs = new runId = fresh analysis
111
+ */
112
+ export function generateRunId(inputs: RunInputs): string {
113
+ const hash = createHash('sha256');
114
+
115
+ // Version ensures cache invalidation on tool updates
116
+ hash.update(`v:${VERSION}`);
117
+
118
+ // Hash repo structure if provided
119
+ if (inputs.repoRoot && existsSync(inputs.repoRoot)) {
120
+ const repoHash = hashDirectory(inputs.repoRoot);
121
+ hash.update(`repo:${repoHash}`);
122
+ }
123
+
124
+ // Hash events file if provided
125
+ if (inputs.eventsPath && existsSync(inputs.eventsPath)) {
126
+ const eventsHash = hashFile(inputs.eventsPath);
127
+ hash.update(`events:${eventsHash}`);
128
+ }
129
+
130
+ // Offline mode affects template loading
131
+ hash.update(`offline:${inputs.offline ?? false}`);
132
+
133
+ return hash.digest('hex').slice(0, 12);
134
+ }
135
+
136
+ /**
137
+ * Get the run directory path
138
+ */
139
+ export function getRunDir(baseDir: string, runId: string): string {
140
+ return join(baseDir, 'runs', runId);
141
+ }
142
+
143
+ /**
144
+ * Create run manifest
145
+ */
146
+ export function createManifest(
147
+ runId: string,
148
+ inputs: RunInputs,
149
+ artifacts: string[],
150
+ status: 'complete' | 'partial' | 'failed'
151
+ ): RunManifest {
152
+ return {
153
+ runId,
154
+ version: VERSION,
155
+ createdAt: new Date().toISOString(),
156
+ inputs: {
157
+ repoRoot: inputs.repoRoot,
158
+ repoHash: inputs.repoRoot && existsSync(inputs.repoRoot)
159
+ ? hashDirectory(inputs.repoRoot)
160
+ : undefined,
161
+ eventsPath: inputs.eventsPath,
162
+ eventsHash: inputs.eventsPath && existsSync(inputs.eventsPath)
163
+ ? hashFile(inputs.eventsPath)
164
+ : undefined,
165
+ offline: inputs.offline ?? false,
166
+ },
167
+ artifacts,
168
+ status,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Load run manifest if it exists
174
+ */
175
+ export function loadManifest(runDir: string): RunManifest | null {
176
+ const manifestPath = join(runDir, 'manifest.json');
177
+
178
+ if (!existsSync(manifestPath)) {
179
+ return null;
180
+ }
181
+
182
+ try {
183
+ const content = readFileSync(manifestPath, 'utf-8');
184
+ return JSON.parse(content) as RunManifest;
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Check if a run can be resumed (all artifacts exist and inputs haven't changed)
192
+ */
193
+ export function canResume(runDir: string, inputs: RunInputs): boolean {
194
+ const manifest = loadManifest(runDir);
195
+
196
+ if (!manifest) {
197
+ return false;
198
+ }
199
+
200
+ // Check status
201
+ if (manifest.status !== 'complete') {
202
+ return false;
203
+ }
204
+
205
+ // Check version match
206
+ if (manifest.version !== VERSION) {
207
+ return false;
208
+ }
209
+
210
+ // Verify input hashes still match
211
+ if (inputs.repoRoot && existsSync(inputs.repoRoot)) {
212
+ const currentRepoHash = hashDirectory(inputs.repoRoot);
213
+ if (currentRepoHash !== manifest.inputs.repoHash) {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ if (inputs.eventsPath && existsSync(inputs.eventsPath)) {
219
+ const currentEventsHash = hashFile(inputs.eventsPath);
220
+ if (currentEventsHash !== manifest.inputs.eventsHash) {
221
+ return false;
222
+ }
223
+ }
224
+
225
+ // Verify all artifacts exist
226
+ for (const artifact of manifest.artifacts) {
227
+ if (!existsSync(join(runDir, artifact))) {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * Load cached artifacts from a previous run
237
+ */
238
+ export function loadCachedArtifacts(runDir: string): CachedArtifacts {
239
+ const artifacts: CachedArtifacts = {};
240
+
241
+ const files = [
242
+ { name: 'inferencemap.json', key: 'inferenceMap' },
243
+ { name: 'insights.json', key: 'insights' },
244
+ { name: 'joined.json', key: 'joined' },
245
+ { name: 'runtime.json', key: 'runtime' },
246
+ ] as const;
247
+
248
+ for (const file of files) {
249
+ const filePath = join(runDir, file.name);
250
+ if (existsSync(filePath)) {
251
+ try {
252
+ const content = readFileSync(filePath, 'utf-8');
253
+ (artifacts as Record<string, unknown>)[file.key] = JSON.parse(content);
254
+ } catch {
255
+ // Skip corrupted files
256
+ }
257
+ }
258
+ }
259
+
260
+ return artifacts;
261
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,450 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { extname } from 'path';
3
+ import { InferenceEvent, RuntimeSummary, ProviderStats, NormalizationOptions, NormalizationResult } from './types.js';
4
+ import { detectFormat, normalizeRuntimeEvents, requiresAgentNormalization } from './format-normalizer.js';
5
+
6
+ // =============================================================================
7
+ // TYPES
8
+ // =============================================================================
9
+
10
+ interface ParseError {
11
+ line: number;
12
+ field: string;
13
+ message: string;
14
+ }
15
+
16
+ // =============================================================================
17
+ // HELPERS
18
+ // =============================================================================
19
+
20
+ function validateEvent(data: unknown, lineNum: number): InferenceEvent {
21
+ if (typeof data !== 'object' || data === null) {
22
+ throw new Error(`Line ${lineNum}: Expected object, got ${typeof data}`);
23
+ }
24
+
25
+ const obj = data as Record<string, unknown>;
26
+ const errors: string[] = [];
27
+
28
+ // Required fields
29
+ if (typeof obj.id !== 'string') {
30
+ errors.push(`Missing or invalid 'id' field`);
31
+ }
32
+ if (typeof obj.ts !== 'string') {
33
+ errors.push(`Missing or invalid 'ts' field`);
34
+ }
35
+ if (typeof obj.provider !== 'string') {
36
+ errors.push(`Missing or invalid 'provider' field`);
37
+ }
38
+ if (typeof obj.model !== 'string') {
39
+ errors.push(`Missing or invalid 'model' field`);
40
+ }
41
+ if (typeof obj.input_tokens !== 'number') {
42
+ errors.push(`Missing or invalid 'input_tokens' field`);
43
+ }
44
+ if (typeof obj.output_tokens !== 'number') {
45
+ errors.push(`Missing or invalid 'output_tokens' field`);
46
+ }
47
+ if (typeof obj.latency_ms !== 'number') {
48
+ errors.push(`Missing or invalid 'latency_ms' field`);
49
+ }
50
+
51
+ if (errors.length > 0) {
52
+ throw new Error(`Line ${lineNum}: ${errors.join(', ')}`);
53
+ }
54
+
55
+ // Build event with optional fields
56
+ const event: InferenceEvent = {
57
+ id: obj.id as string,
58
+ ts: obj.ts as string,
59
+ provider: obj.provider as InferenceEvent['provider'],
60
+ model: obj.model as string,
61
+ input_tokens: obj.input_tokens as number,
62
+ output_tokens: obj.output_tokens as number,
63
+ latency_ms: obj.latency_ms as number,
64
+ intent: obj.intent as string | undefined,
65
+ callsite_id: obj.callsite_id as string | undefined,
66
+ // Runtime pattern fields
67
+ streaming: typeof obj.streaming === 'boolean' ? obj.streaming : undefined,
68
+ ttft_ms: typeof obj.ttft_ms === 'number' ? obj.ttft_ms : undefined,
69
+ batch_size: typeof obj.batch_size === 'number' ? obj.batch_size : undefined,
70
+ batch_id: typeof obj.batch_id === 'string' ? obj.batch_id : undefined,
71
+ cached: typeof obj.cached === 'boolean' ? obj.cached : undefined,
72
+ retry_count: typeof obj.retry_count === 'number' ? obj.retry_count : undefined,
73
+ fallback_used: typeof obj.fallback_used === 'boolean' ? obj.fallback_used : undefined,
74
+ original_model: typeof obj.original_model === 'string' ? obj.original_model : undefined,
75
+ };
76
+
77
+ // Infer streaming if not explicitly provided but ttft_ms is present
78
+ if (event.streaming === undefined && event.ttft_ms !== undefined) {
79
+ event.streaming = true;
80
+ }
81
+
82
+ return event;
83
+ }
84
+
85
+ /**
86
+ * Detect streaming based on heuristics when not explicitly provided.
87
+ *
88
+ * Heuristics:
89
+ * 1. If ttft_ms is present and much smaller than total latency → streaming
90
+ * 2. If output_tokens/latency_ms ratio suggests real-time generation → likely streaming
91
+ * 3. If latency pattern shows consistent per-token timing → streaming
92
+ */
93
+ function inferStreamingFromHeuristics(events: InferenceEvent[]): InferenceEvent[] {
94
+ return events.map(event => {
95
+ // If streaming is already known, skip
96
+ if (event.streaming !== undefined) {
97
+ return event;
98
+ }
99
+
100
+ // Heuristic 1: If ttft_ms present and < 50% of total latency, likely streaming
101
+ if (event.ttft_ms !== undefined) {
102
+ event.streaming = event.ttft_ms < event.latency_ms * 0.5;
103
+ return event;
104
+ }
105
+
106
+ // Heuristic 2: Calculate tokens per second
107
+ // Streaming typically shows 20-150 TPS, non-streaming shows all tokens at once
108
+ const outputTokens = event.output_tokens;
109
+ const latencySeconds = event.latency_ms / 1000;
110
+ const tps = outputTokens / latencySeconds;
111
+
112
+ // Very high TPS (>200) with significant output suggests non-streaming (batch return)
113
+ // Very low TPS (<5) with significant output suggests non-streaming (slow model, one response)
114
+ // TPS in 20-150 range with reasonable output suggests streaming
115
+ if (outputTokens > 50) {
116
+ if (tps >= 20 && tps <= 150) {
117
+ // Likely streaming - consistent token generation rate
118
+ event.streaming = true;
119
+ } else if (tps > 200) {
120
+ // Likely non-streaming - tokens returned all at once (cached or fast response)
121
+ event.streaming = false;
122
+ }
123
+ // If TPS < 20, could be either - leave as undefined
124
+ }
125
+
126
+ return event;
127
+ });
128
+ }
129
+
130
+ function parseJSONL(content: string): InferenceEvent[] {
131
+ const lines = content.trim().split('\n').filter(l => l.trim());
132
+ const events: InferenceEvent[] = [];
133
+
134
+ for (let i = 0; i < lines.length; i++) {
135
+ const line = lines[i].trim();
136
+ if (!line) continue;
137
+
138
+ try {
139
+ const data = JSON.parse(line);
140
+ events.push(validateEvent(data, i + 1));
141
+ } catch (error) {
142
+ if (error instanceof SyntaxError) {
143
+ throw new Error(`Line ${i + 1}: Invalid JSON`);
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ return events;
150
+ }
151
+
152
+ function parseJSONArray(content: string): InferenceEvent[] {
153
+ let data: unknown[];
154
+ try {
155
+ data = JSON.parse(content);
156
+ } catch {
157
+ throw new Error('Invalid JSON');
158
+ }
159
+
160
+ if (!Array.isArray(data)) {
161
+ throw new Error('Expected JSON array');
162
+ }
163
+
164
+ return data.map((item, i) => validateEvent(item, i + 1));
165
+ }
166
+
167
+ function parseCSV(content: string): InferenceEvent[] {
168
+ const lines = content.trim().split('\n');
169
+ if (lines.length < 2) {
170
+ return [];
171
+ }
172
+
173
+ const headers = lines[0].split(',').map(h => h.trim());
174
+ const events: InferenceEvent[] = [];
175
+
176
+ for (let i = 1; i < lines.length; i++) {
177
+ const values = lines[i].split(',').map(v => v.trim());
178
+ const obj: Record<string, unknown> = {};
179
+
180
+ for (let j = 0; j < headers.length; j++) {
181
+ const header = headers[j];
182
+ const value = values[j];
183
+
184
+ // Convert numeric fields
185
+ if (['input_tokens', 'output_tokens', 'latency_ms'].includes(header)) {
186
+ obj[header] = parseFloat(value);
187
+ } else {
188
+ obj[header] = value;
189
+ }
190
+ }
191
+
192
+ events.push(validateEvent(obj, i + 1));
193
+ }
194
+
195
+ return events;
196
+ }
197
+
198
+ // =============================================================================
199
+ // PUBLIC API
200
+ // =============================================================================
201
+
202
+ /**
203
+ * Extended parse result with normalization metadata.
204
+ */
205
+ export interface ParseResult {
206
+ events: InferenceEvent[];
207
+ normalization?: NormalizationResult;
208
+ warnings: string[];
209
+ }
210
+
211
+ /**
212
+ * Parse runtime events from a file.
213
+ *
214
+ * This function implements the format detection pipeline (PRD §6.4):
215
+ * 1. Try direct parsing for known formats (JSONL, JSON, CSV)
216
+ * 2. Fall back to agent-based normalization for unknown formats
217
+ * 3. Apply streaming inference heuristics
218
+ *
219
+ * @param path - Path to the events file
220
+ * @param options - Normalization options (format hints, mappings, etc.)
221
+ */
222
+ export async function parseEvents(
223
+ path: string,
224
+ options: NormalizationOptions = {},
225
+ ): Promise<InferenceEvent[]> {
226
+ const result = await parseEventsWithMetadata(path, options);
227
+ return result.events;
228
+ }
229
+
230
+ /**
231
+ * Parse runtime events with full normalization metadata.
232
+ * Use this for detailed reporting on format detection and field mappings.
233
+ */
234
+ export async function parseEventsWithMetadata(
235
+ path: string,
236
+ options: NormalizationOptions = {},
237
+ ): Promise<ParseResult> {
238
+ if (!existsSync(path)) {
239
+ throw new Error(`File not found: ${path}`);
240
+ }
241
+
242
+ const content = readFileSync(path, 'utf-8');
243
+ const ext = extname(path).toLowerCase();
244
+ const warnings: string[] = [];
245
+
246
+ // Step 1: Try direct parsing for standard formats
247
+ if (!options.format_hint) {
248
+ try {
249
+ let events: InferenceEvent[] | null = null;
250
+
251
+ switch (ext) {
252
+ case '.jsonl':
253
+ case '.ndjson':
254
+ events = parseJSONL(content);
255
+ break;
256
+ case '.json':
257
+ // Could be JSON array or complex format (OTEL, Jaeger, etc.)
258
+ if (content.trim().startsWith('[')) {
259
+ try {
260
+ events = parseJSONArray(content);
261
+ } catch {
262
+ // JSON array but not InferenceEvent schema - needs normalization
263
+ }
264
+ }
265
+ break;
266
+ case '.csv':
267
+ events = parseCSV(content);
268
+ break;
269
+ case '.tsv':
270
+ events = parseTSV(content);
271
+ break;
272
+ }
273
+
274
+ if (events && events.length > 0) {
275
+ // Direct parse succeeded
276
+ return {
277
+ events: inferStreamingFromHeuristics(events),
278
+ warnings,
279
+ };
280
+ }
281
+ } catch (error) {
282
+ // Direct parse failed - will try format normalization
283
+ warnings.push(`Direct parse failed: ${error instanceof Error ? error.message : String(error)}`);
284
+ }
285
+ }
286
+
287
+ // Step 2: Detect format and check if agent normalization is needed
288
+ const detection = detectFormat(content, path);
289
+
290
+ // If user provided strict mode and we need agent, fail early
291
+ if (options.strict && detection.requires_agent) {
292
+ throw new Error(
293
+ `Format "${detection.format_type}" requires agent normalization. ` +
294
+ `Use --format to specify the format, or remove --strict flag.`
295
+ );
296
+ }
297
+
298
+ // If direct-parse format detected with low confidence, warn but try
299
+ if (!detection.requires_agent && detection.confidence < 0.9) {
300
+ warnings.push(
301
+ `Format detection confidence is ${(detection.confidence * 100).toFixed(0)}%. ` +
302
+ `Results may be incomplete.`
303
+ );
304
+ }
305
+
306
+ // Step 3: Use format normalizer for complex formats
307
+ if (detection.requires_agent || options.format_hint) {
308
+ const { events, normalization, errors } = await normalizeRuntimeEvents(content, options);
309
+
310
+ if (errors.length > 0 && options.strict) {
311
+ throw new Error(`Normalization errors: ${errors.slice(0, 3).join('; ')}`);
312
+ }
313
+
314
+ warnings.push(...normalization.warnings);
315
+
316
+ return {
317
+ events: inferStreamingFromHeuristics(events),
318
+ normalization,
319
+ warnings,
320
+ };
321
+ }
322
+
323
+ // Step 4: Final fallback - try auto-detection
324
+ let events: InferenceEvent[];
325
+
326
+ if (content.trim().startsWith('[')) {
327
+ events = parseJSONArray(content);
328
+ } else if (content.trim().startsWith('{')) {
329
+ events = parseJSONL(content);
330
+ } else {
331
+ throw new Error(
332
+ `Unknown file format: ${ext}. ` +
333
+ `Supported formats: .jsonl, .json, .csv, .tsv, or observability exports (OTEL, Jaeger, Zipkin). ` +
334
+ `Use --format to specify the format type.`
335
+ );
336
+ }
337
+
338
+ return {
339
+ events: inferStreamingFromHeuristics(events),
340
+ warnings,
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Parse TSV (tab-separated values) format.
346
+ */
347
+ function parseTSV(content: string): InferenceEvent[] {
348
+ const lines = content.trim().split('\n');
349
+ if (lines.length < 2) {
350
+ return [];
351
+ }
352
+
353
+ const headers = lines[0].split('\t').map(h => h.trim());
354
+ const events: InferenceEvent[] = [];
355
+
356
+ for (let i = 1; i < lines.length; i++) {
357
+ const values = lines[i].split('\t').map(v => v.trim());
358
+ const obj: Record<string, unknown> = {};
359
+
360
+ for (let j = 0; j < headers.length; j++) {
361
+ const header = headers[j];
362
+ const value = values[j];
363
+
364
+ // Convert numeric fields
365
+ if (['input_tokens', 'output_tokens', 'latency_ms', 'ttft_ms', 'batch_size', 'retry_count'].includes(header)) {
366
+ obj[header] = parseFloat(value);
367
+ } else if (['streaming', 'cached', 'fallback_used'].includes(header)) {
368
+ obj[header] = value.toLowerCase() === 'true';
369
+ } else {
370
+ obj[header] = value;
371
+ }
372
+ }
373
+
374
+ events.push(validateEvent(obj, i + 1));
375
+ }
376
+
377
+ return events;
378
+ }
379
+
380
+ export function percentile(values: number[], p: number): number {
381
+ if (values.length === 0) return 0;
382
+
383
+ const sorted = [...values].sort((a, b) => a - b);
384
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
385
+ return sorted[Math.max(0, index)];
386
+ }
387
+
388
+ export function aggregate(events: InferenceEvent[]): RuntimeSummary {
389
+ if (events.length === 0) {
390
+ return {
391
+ totalEvents: 0,
392
+ byProvider: {},
393
+ byModel: {},
394
+ global: { p50: 0, p95: 0, p99: 0 },
395
+ };
396
+ }
397
+
398
+ const byProvider: Record<string, InferenceEvent[]> = {};
399
+ const byModel: Record<string, InferenceEvent[]> = {};
400
+ const allLatencies: number[] = [];
401
+
402
+ for (const event of events) {
403
+ // Group by provider
404
+ if (!byProvider[event.provider]) {
405
+ byProvider[event.provider] = [];
406
+ }
407
+ byProvider[event.provider].push(event);
408
+
409
+ // Group by model
410
+ if (!byModel[event.model]) {
411
+ byModel[event.model] = [];
412
+ }
413
+ byModel[event.model].push(event);
414
+
415
+ allLatencies.push(event.latency_ms);
416
+ }
417
+
418
+ const computeStats = (group: InferenceEvent[]): ProviderStats => {
419
+ const latencies = group.map(e => e.latency_ms);
420
+ return {
421
+ calls: group.length,
422
+ tokens_in: group.reduce((sum, e) => sum + e.input_tokens, 0),
423
+ tokens_out: group.reduce((sum, e) => sum + e.output_tokens, 0),
424
+ latency_p50: percentile(latencies, 50),
425
+ latency_p95: percentile(latencies, 95),
426
+ latency_p99: percentile(latencies, 99),
427
+ };
428
+ };
429
+
430
+ const providerStats: Record<string, ProviderStats> = {};
431
+ for (const [provider, group] of Object.entries(byProvider)) {
432
+ providerStats[provider] = computeStats(group);
433
+ }
434
+
435
+ const modelStats: Record<string, ProviderStats> = {};
436
+ for (const [model, group] of Object.entries(byModel)) {
437
+ modelStats[model] = computeStats(group);
438
+ }
439
+
440
+ return {
441
+ totalEvents: events.length,
442
+ byProvider: providerStats,
443
+ byModel: modelStats,
444
+ global: {
445
+ p50: percentile(allLatencies, 50),
446
+ p95: percentile(allLatencies, 95),
447
+ p99: percentile(allLatencies, 99),
448
+ },
449
+ };
450
+ }