@artemiskit/core 0.2.4 → 0.4.0

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 (81) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +4 -0
  3. package/dist/adapters/registry.d.ts.map +1 -1
  4. package/dist/adapters/types.d.ts +50 -2
  5. package/dist/adapters/types.d.ts.map +1 -1
  6. package/dist/agent-evaluation/index.d.ts +3 -0
  7. package/dist/agent-evaluation/index.d.ts.map +1 -0
  8. package/dist/agent-evaluation/scorer.d.ts +35 -0
  9. package/dist/agent-evaluation/scorer.d.ts.map +1 -0
  10. package/dist/agent-evaluation/types.d.ts +37 -0
  11. package/dist/agent-evaluation/types.d.ts.map +1 -0
  12. package/dist/artifacts/manifest.d.ts.map +1 -1
  13. package/dist/artifacts/types.d.ts +54 -0
  14. package/dist/artifacts/types.d.ts.map +1 -1
  15. package/dist/evaluators/index.d.ts +1 -0
  16. package/dist/evaluators/index.d.ts.map +1 -1
  17. package/dist/evaluators/json-schema.d.ts +0 -1
  18. package/dist/evaluators/json-schema.d.ts.map +1 -1
  19. package/dist/evaluators/llm-grader.d.ts +2 -0
  20. package/dist/evaluators/llm-grader.d.ts.map +1 -1
  21. package/dist/evaluators/tool-trace.d.ts +7 -0
  22. package/dist/evaluators/tool-trace.d.ts.map +1 -0
  23. package/dist/evaluators/types.d.ts +20 -0
  24. package/dist/evaluators/types.d.ts.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +19964 -12482
  28. package/dist/runner/executor.d.ts.map +1 -1
  29. package/dist/runner/runner.d.ts.map +1 -1
  30. package/dist/runner/types.d.ts +4 -0
  31. package/dist/runner/types.d.ts.map +1 -1
  32. package/dist/scenario/schema.d.ts +789 -99
  33. package/dist/scenario/schema.d.ts.map +1 -1
  34. package/dist/storage/supabase.d.ts +26 -5
  35. package/dist/storage/supabase.d.ts.map +1 -1
  36. package/dist/storage/types.d.ts +167 -1
  37. package/dist/storage/types.d.ts.map +1 -1
  38. package/dist/tools/fixture-executor.d.ts +10 -0
  39. package/dist/tools/fixture-executor.d.ts.map +1 -0
  40. package/dist/tools/index.d.ts +4 -0
  41. package/dist/tools/index.d.ts.map +1 -0
  42. package/dist/tools/schema-validator.d.ts +10 -0
  43. package/dist/tools/schema-validator.d.ts.map +1 -0
  44. package/dist/tools/types.d.ts +50 -0
  45. package/dist/tools/types.d.ts.map +1 -0
  46. package/package.json +2 -1
  47. package/src/adapters/registry.ts +45 -0
  48. package/src/adapters/types.test.ts +21 -0
  49. package/src/adapters/types.ts +56 -0
  50. package/src/agent-evaluation/index.ts +2 -0
  51. package/src/agent-evaluation/scorer.test.ts +1194 -0
  52. package/src/agent-evaluation/scorer.ts +640 -0
  53. package/src/agent-evaluation/types.test.ts +27 -0
  54. package/src/agent-evaluation/types.ts +43 -0
  55. package/src/artifacts/manifest.test.ts +66 -19
  56. package/src/artifacts/manifest.ts +18 -5
  57. package/src/artifacts/types.ts +65 -0
  58. package/src/evaluators/index.ts +3 -0
  59. package/src/evaluators/json-schema.test.ts +130 -0
  60. package/src/evaluators/json-schema.ts +38 -63
  61. package/src/evaluators/llm-grader.test.ts +80 -0
  62. package/src/evaluators/llm-grader.ts +44 -6
  63. package/src/evaluators/tool-trace.test.ts +46 -0
  64. package/src/evaluators/tool-trace.ts +50 -0
  65. package/src/evaluators/types.ts +20 -0
  66. package/src/index.ts +6 -0
  67. package/src/runner/executor.test.ts +340 -0
  68. package/src/runner/executor.ts +289 -20
  69. package/src/runner/release-validation.test.ts +169 -0
  70. package/src/runner/runner.ts +5 -1
  71. package/src/runner/types.ts +4 -0
  72. package/src/scenario/schema.ts +66 -1
  73. package/src/storage/supabase.test.ts +1052 -0
  74. package/src/storage/supabase.ts +614 -5
  75. package/src/storage/types.ts +207 -1
  76. package/src/tools/fixture-executor.test.ts +88 -0
  77. package/src/tools/fixture-executor.ts +112 -0
  78. package/src/tools/index.ts +3 -0
  79. package/src/tools/schema-validator.test.ts +32 -0
  80. package/src/tools/schema-validator.ts +56 -0
  81. package/src/tools/types.ts +80 -0
@@ -1,10 +1,21 @@
1
1
  /**
2
- * Supabase storage adapter
2
+ * Supabase storage adapter with analytics capabilities
3
3
  */
4
4
 
5
5
  import { type SupabaseClient, createClient } from '@supabase/supabase-js';
6
- import type { RunManifest } from '../artifacts/types';
7
- import type { ComparisonResult, ListOptions, RunListItem, StorageAdapter } from './types';
6
+ import { type CaseResult, type RunManifest, getCaseEvaluationStatus } from '../artifacts/types';
7
+ import type {
8
+ AnalyticsStorageAdapter,
9
+ BaselineMetadata,
10
+ CaseResultQueryOptions,
11
+ CaseResultRecord,
12
+ ComparisonResult,
13
+ ListOptions,
14
+ MetricsSnapshot,
15
+ MetricsTrendOptions,
16
+ RunListItem,
17
+ TrendDataPoint,
18
+ } from './types';
8
19
 
9
20
  export interface SupabaseStorageConfig {
10
21
  url: string;
@@ -12,15 +23,45 @@ export interface SupabaseStorageConfig {
12
23
  bucket?: string;
13
24
  }
14
25
 
15
- export class SupabaseStorageAdapter implements StorageAdapter {
26
+ /**
27
+ * Map CaseResult from manifest to CaseResultRecord for storage
28
+ */
29
+ function mapCaseToRecord(runId: string, caseResult: CaseResult): CaseResultRecord {
30
+ return {
31
+ runId,
32
+ caseId: caseResult.id,
33
+ caseName: caseResult.name,
34
+ status: getCaseEvaluationStatus(caseResult),
35
+ attempts: caseResult.attempts ?? 1,
36
+ score: caseResult.score,
37
+ matcherType: caseResult.matcherType,
38
+ reason: caseResult.reason,
39
+ response: caseResult.response,
40
+ latencyMs: caseResult.latencyMs,
41
+ promptTokens: caseResult.tokens.prompt,
42
+ completionTokens: caseResult.tokens.completion,
43
+ totalTokens: caseResult.tokens.total,
44
+ error: caseResult.error,
45
+ evidence: caseResult.evidence,
46
+ tags: caseResult.tags,
47
+ };
48
+ }
49
+
50
+ export class SupabaseStorageAdapter implements AnalyticsStorageAdapter {
16
51
  private client: SupabaseClient;
17
52
  private bucket: string;
53
+ private project: string;
18
54
 
19
- constructor(config: SupabaseStorageConfig) {
55
+ constructor(config: SupabaseStorageConfig, project?: string) {
20
56
  this.client = createClient(config.url, config.anonKey);
21
57
  this.bucket = config.bucket || 'artemis-runs';
58
+ this.project = project || 'default';
22
59
  }
23
60
 
61
+ // ============================================================================
62
+ // Core Storage Methods
63
+ // ============================================================================
64
+
24
65
  async save(manifest: RunManifest): Promise<string> {
25
66
  const filePath = `${manifest.project}/${manifest.run_id}.json`;
26
67
 
@@ -45,6 +86,11 @@ export class SupabaseStorageAdapter implements StorageAdapter {
45
86
  total_cases: manifest.metrics.total_cases,
46
87
  passed_cases: manifest.metrics.passed_cases,
47
88
  failed_cases: manifest.metrics.failed_cases,
89
+ total_attempts: manifest.metrics.total_attempts ?? manifest.metrics.total_cases,
90
+ valid_evaluations: manifest.metrics.valid_evaluations ?? manifest.metrics.total_cases,
91
+ invalid_evaluations: manifest.metrics.invalid_evaluations ?? 0,
92
+ outcome_rate_denominator:
93
+ manifest.metrics.outcome_rate_denominator ?? manifest.metrics.total_cases,
48
94
  median_latency_ms: manifest.metrics.median_latency_ms,
49
95
  p95_latency_ms: manifest.metrics.p95_latency_ms,
50
96
  total_tokens: manifest.metrics.total_tokens,
@@ -62,6 +108,12 @@ export class SupabaseStorageAdapter implements StorageAdapter {
62
108
  throw new Error(`Failed to save run metadata: ${dbError.message}`);
63
109
  }
64
110
 
111
+ // Also save individual case results for granular analytics
112
+ if (manifest.cases && manifest.cases.length > 0) {
113
+ const caseRecords = manifest.cases.map((c) => mapCaseToRecord(manifest.run_id, c));
114
+ await this.saveCaseResults(caseRecords);
115
+ }
116
+
65
117
  return filePath;
66
118
  }
67
119
 
@@ -132,6 +184,7 @@ export class SupabaseStorageAdapter implements StorageAdapter {
132
184
  await this.client.storage.from(this.bucket).remove([run.manifest_path]);
133
185
  }
134
186
 
187
+ // Case results are deleted via CASCADE
135
188
  await this.client.from('runs').delete().eq('run_id', runId);
136
189
  }
137
190
 
@@ -148,4 +201,560 @@ export class SupabaseStorageAdapter implements StorageAdapter {
148
201
  },
149
202
  };
150
203
  }
204
+
205
+ // ============================================================================
206
+ // Baseline Methods
207
+ // ============================================================================
208
+
209
+ async setBaseline(scenario: string, runId: string, tag?: string): Promise<BaselineMetadata> {
210
+ // Load the run to get metrics
211
+ const { data: run, error: runError } = await this.client
212
+ .from('runs')
213
+ .select('*')
214
+ .eq('run_id', runId)
215
+ .single();
216
+
217
+ if (runError || !run) {
218
+ throw new Error(`Run not found: ${runId}`);
219
+ }
220
+
221
+ const baselineData = {
222
+ project: run.project,
223
+ scenario,
224
+ run_id: runId,
225
+ success_rate: run.success_rate,
226
+ median_latency_ms: run.median_latency_ms,
227
+ total_tokens: run.total_tokens,
228
+ passed_cases: run.passed_cases,
229
+ failed_cases: run.failed_cases,
230
+ total_cases: run.total_cases,
231
+ tag,
232
+ created_by: run.run_by,
233
+ };
234
+
235
+ const { error } = await this.client.from('baselines').upsert(baselineData, {
236
+ onConflict: 'project,scenario',
237
+ });
238
+
239
+ if (error) {
240
+ throw new Error(`Failed to set baseline: ${error.message}`);
241
+ }
242
+
243
+ return {
244
+ scenario,
245
+ runId,
246
+ createdAt: new Date().toISOString(),
247
+ metrics: {
248
+ successRate: run.success_rate,
249
+ medianLatencyMs: run.median_latency_ms,
250
+ totalTokens: run.total_tokens,
251
+ passedCases: run.passed_cases,
252
+ failedCases: run.failed_cases,
253
+ totalCases: run.total_cases,
254
+ },
255
+ tag,
256
+ };
257
+ }
258
+
259
+ async getBaseline(scenario: string): Promise<BaselineMetadata | null> {
260
+ const { data, error } = await this.client
261
+ .from('baselines')
262
+ .select('*')
263
+ .eq('project', this.project)
264
+ .eq('scenario', scenario)
265
+ .single();
266
+
267
+ if (error || !data) {
268
+ return null;
269
+ }
270
+
271
+ return {
272
+ scenario: data.scenario,
273
+ runId: data.run_id,
274
+ createdAt: data.created_at,
275
+ metrics: {
276
+ successRate: data.success_rate,
277
+ medianLatencyMs: data.median_latency_ms,
278
+ totalTokens: data.total_tokens,
279
+ passedCases: data.passed_cases,
280
+ failedCases: data.failed_cases,
281
+ totalCases: data.total_cases,
282
+ },
283
+ tag: data.tag,
284
+ };
285
+ }
286
+
287
+ async getBaselineByRunId(runId: string): Promise<BaselineMetadata | null> {
288
+ const { data, error } = await this.client
289
+ .from('baselines')
290
+ .select('*')
291
+ .eq('run_id', runId)
292
+ .single();
293
+
294
+ if (error || !data) {
295
+ return null;
296
+ }
297
+
298
+ return {
299
+ scenario: data.scenario,
300
+ runId: data.run_id,
301
+ createdAt: data.created_at,
302
+ metrics: {
303
+ successRate: data.success_rate,
304
+ medianLatencyMs: data.median_latency_ms,
305
+ totalTokens: data.total_tokens,
306
+ passedCases: data.passed_cases,
307
+ failedCases: data.failed_cases,
308
+ totalCases: data.total_cases,
309
+ },
310
+ tag: data.tag,
311
+ };
312
+ }
313
+
314
+ async listBaselines(): Promise<BaselineMetadata[]> {
315
+ const { data, error } = await this.client
316
+ .from('baselines')
317
+ .select('*')
318
+ .eq('project', this.project)
319
+ .order('created_at', { ascending: false });
320
+
321
+ if (error) {
322
+ throw new Error(`Failed to list baselines: ${error.message}`);
323
+ }
324
+
325
+ return (data || []).map((b) => ({
326
+ scenario: b.scenario,
327
+ runId: b.run_id,
328
+ createdAt: b.created_at,
329
+ metrics: {
330
+ successRate: b.success_rate,
331
+ medianLatencyMs: b.median_latency_ms,
332
+ totalTokens: b.total_tokens,
333
+ passedCases: b.passed_cases,
334
+ failedCases: b.failed_cases,
335
+ totalCases: b.total_cases,
336
+ },
337
+ tag: b.tag,
338
+ }));
339
+ }
340
+
341
+ async removeBaseline(scenario: string): Promise<boolean> {
342
+ const { error, count } = await this.client
343
+ .from('baselines')
344
+ .delete()
345
+ .eq('project', this.project)
346
+ .eq('scenario', scenario);
347
+
348
+ if (error) {
349
+ throw new Error(`Failed to remove baseline: ${error.message}`);
350
+ }
351
+
352
+ return (count ?? 0) > 0;
353
+ }
354
+
355
+ async removeBaselineByRunId(runId: string): Promise<boolean> {
356
+ const { error, count } = await this.client.from('baselines').delete().eq('run_id', runId);
357
+
358
+ if (error) {
359
+ throw new Error(`Failed to remove baseline: ${error.message}`);
360
+ }
361
+
362
+ return (count ?? 0) > 0;
363
+ }
364
+
365
+ async compareToBaseline(
366
+ runId: string,
367
+ regressionThreshold = 0.05
368
+ ): Promise<{
369
+ baseline: BaselineMetadata;
370
+ comparison: ComparisonResult;
371
+ hasRegression: boolean;
372
+ regressionThreshold: number;
373
+ } | null> {
374
+ // Get the run's scenario
375
+ const { data: run, error: runError } = await this.client
376
+ .from('runs')
377
+ .select('scenario')
378
+ .eq('run_id', runId)
379
+ .single();
380
+
381
+ if (runError || !run) {
382
+ return null;
383
+ }
384
+
385
+ // Get the baseline for this scenario
386
+ const baseline = await this.getBaseline(run.scenario);
387
+ if (!baseline) {
388
+ return null;
389
+ }
390
+
391
+ // Compare
392
+ const comparison = await this.compare(baseline.runId, runId);
393
+
394
+ // Check for regression (success rate dropped by more than threshold)
395
+ const hasRegression = comparison.delta.successRate < -regressionThreshold;
396
+
397
+ return {
398
+ baseline,
399
+ comparison,
400
+ hasRegression,
401
+ regressionThreshold,
402
+ };
403
+ }
404
+
405
+ // ============================================================================
406
+ // Case Results Methods
407
+ // ============================================================================
408
+
409
+ async saveCaseResult(result: CaseResultRecord): Promise<string> {
410
+ const dbRecord = {
411
+ run_id: result.runId,
412
+ case_id: result.caseId,
413
+ case_name: result.caseName,
414
+ status: result.status,
415
+ attempts: result.attempts ?? 1,
416
+ score: result.score,
417
+ matcher_type: result.matcherType,
418
+ reason: result.reason,
419
+ response: result.response,
420
+ latency_ms: result.latencyMs,
421
+ prompt_tokens: result.promptTokens,
422
+ completion_tokens: result.completionTokens,
423
+ total_tokens: result.totalTokens,
424
+ error: result.error,
425
+ evidence: result.evidence,
426
+ tags: result.tags || [],
427
+ };
428
+
429
+ const { data, error } = await this.client
430
+ .from('case_results')
431
+ .upsert(dbRecord, { onConflict: 'run_id,case_id' })
432
+ .select('id')
433
+ .single();
434
+
435
+ if (error) {
436
+ throw new Error(`Failed to save case result: ${error.message}`);
437
+ }
438
+
439
+ return data?.id || result.caseId;
440
+ }
441
+
442
+ async saveCaseResults(results: CaseResultRecord[]): Promise<string[]> {
443
+ if (results.length === 0) {
444
+ return [];
445
+ }
446
+
447
+ const dbRecords = results.map((r) => ({
448
+ run_id: r.runId,
449
+ case_id: r.caseId,
450
+ case_name: r.caseName,
451
+ status: r.status,
452
+ attempts: r.attempts ?? 1,
453
+ score: r.score,
454
+ matcher_type: r.matcherType,
455
+ reason: r.reason,
456
+ response: r.response,
457
+ latency_ms: r.latencyMs,
458
+ prompt_tokens: r.promptTokens,
459
+ completion_tokens: r.completionTokens,
460
+ total_tokens: r.totalTokens,
461
+ error: r.error,
462
+ evidence: r.evidence,
463
+ tags: r.tags || [],
464
+ }));
465
+
466
+ const { data, error } = await this.client
467
+ .from('case_results')
468
+ .upsert(dbRecords, { onConflict: 'run_id,case_id' })
469
+ .select('id');
470
+
471
+ if (error) {
472
+ throw new Error(`Failed to save case results: ${error.message}`);
473
+ }
474
+
475
+ return (data || []).map((d) => d.id);
476
+ }
477
+
478
+ async getCaseResults(runId: string): Promise<CaseResultRecord[]> {
479
+ const { data, error } = await this.client
480
+ .from('case_results')
481
+ .select('*')
482
+ .eq('run_id', runId)
483
+ .order('created_at', { ascending: true });
484
+
485
+ if (error) {
486
+ throw new Error(`Failed to get case results: ${error.message}`);
487
+ }
488
+
489
+ return (data || []).map((r) => ({
490
+ id: r.id,
491
+ runId: r.run_id,
492
+ caseId: r.case_id,
493
+ caseName: r.case_name,
494
+ status: r.status,
495
+ attempts: r.attempts,
496
+ score: r.score,
497
+ matcherType: r.matcher_type,
498
+ reason: r.reason,
499
+ response: r.response,
500
+ latencyMs: r.latency_ms,
501
+ promptTokens: r.prompt_tokens,
502
+ completionTokens: r.completion_tokens,
503
+ totalTokens: r.total_tokens,
504
+ error: r.error,
505
+ evidence: r.evidence,
506
+ tags: r.tags,
507
+ createdAt: r.created_at,
508
+ }));
509
+ }
510
+
511
+ async queryCaseResults(options: CaseResultQueryOptions): Promise<CaseResultRecord[]> {
512
+ let query = this.client
513
+ .from('case_results')
514
+ .select('*')
515
+ .order('created_at', { ascending: false });
516
+
517
+ if (options.runId) {
518
+ query = query.eq('run_id', options.runId);
519
+ }
520
+ if (options.caseId) {
521
+ query = query.eq('case_id', options.caseId);
522
+ }
523
+ if (options.status) {
524
+ query = query.eq('status', options.status);
525
+ }
526
+ if (options.tags && options.tags.length > 0) {
527
+ query = query.overlaps('tags', options.tags);
528
+ }
529
+ if (options.offset && options.limit) {
530
+ query = query.range(options.offset, options.offset + options.limit - 1);
531
+ } else if (options.limit) {
532
+ query = query.limit(options.limit);
533
+ }
534
+
535
+ const { data, error } = await query;
536
+
537
+ if (error) {
538
+ throw new Error(`Failed to query case results: ${error.message}`);
539
+ }
540
+
541
+ return (data || []).map((r) => ({
542
+ id: r.id,
543
+ runId: r.run_id,
544
+ caseId: r.case_id,
545
+ caseName: r.case_name,
546
+ status: r.status,
547
+ attempts: r.attempts,
548
+ score: r.score,
549
+ matcherType: r.matcher_type,
550
+ reason: r.reason,
551
+ response: r.response,
552
+ latencyMs: r.latency_ms,
553
+ promptTokens: r.prompt_tokens,
554
+ completionTokens: r.completion_tokens,
555
+ totalTokens: r.total_tokens,
556
+ error: r.error,
557
+ evidence: r.evidence,
558
+ tags: r.tags,
559
+ createdAt: r.created_at,
560
+ }));
561
+ }
562
+
563
+ // ============================================================================
564
+ // Metrics History Methods
565
+ // ============================================================================
566
+
567
+ async saveMetricsSnapshot(snapshot: MetricsSnapshot): Promise<string> {
568
+ const dbRecord = {
569
+ date: snapshot.date,
570
+ project: snapshot.project,
571
+ scenario: snapshot.scenario || null,
572
+ total_runs: snapshot.totalRuns,
573
+ total_cases: snapshot.totalCases,
574
+ passed_cases: snapshot.passedCases,
575
+ failed_cases: snapshot.failedCases,
576
+ avg_success_rate: snapshot.avgSuccessRate,
577
+ avg_latency_ms: snapshot.avgLatencyMs,
578
+ avg_tokens_per_run: snapshot.avgTokensPerRun,
579
+ min_success_rate: snapshot.minSuccessRate,
580
+ max_success_rate: snapshot.maxSuccessRate,
581
+ min_latency_ms: snapshot.minLatencyMs,
582
+ max_latency_ms: snapshot.maxLatencyMs,
583
+ total_tokens: snapshot.totalTokens,
584
+ };
585
+
586
+ const { data, error } = await this.client
587
+ .from('metrics_history')
588
+ .upsert(dbRecord, { onConflict: 'date,project,scenario' })
589
+ .select('id')
590
+ .single();
591
+
592
+ if (error) {
593
+ throw new Error(`Failed to save metrics snapshot: ${error.message}`);
594
+ }
595
+
596
+ return data?.id || `${snapshot.date}-${snapshot.project}`;
597
+ }
598
+
599
+ async getMetricsTrend(options: MetricsTrendOptions): Promise<TrendDataPoint[]> {
600
+ let query = this.client
601
+ .from('metrics_history')
602
+ .select('date, avg_success_rate, avg_latency_ms, total_runs, total_tokens')
603
+ .eq('project', options.project)
604
+ .order('date', { ascending: true });
605
+
606
+ if (options.scenario) {
607
+ query = query.eq('scenario', options.scenario);
608
+ } else {
609
+ query = query.is('scenario', null);
610
+ }
611
+
612
+ if (options.startDate) {
613
+ query = query.gte('date', options.startDate);
614
+ }
615
+ if (options.endDate) {
616
+ query = query.lte('date', options.endDate);
617
+ }
618
+ if (options.limit) {
619
+ query = query.limit(options.limit);
620
+ }
621
+
622
+ const { data, error } = await query;
623
+
624
+ if (error) {
625
+ throw new Error(`Failed to get metrics trend: ${error.message}`);
626
+ }
627
+
628
+ return (data || []).map((m) => ({
629
+ date: m.date,
630
+ successRate: m.avg_success_rate,
631
+ latencyMs: m.avg_latency_ms,
632
+ totalRuns: m.total_runs,
633
+ totalTokens: m.total_tokens,
634
+ }));
635
+ }
636
+
637
+ async getMetricsSnapshot(
638
+ date: string,
639
+ project: string,
640
+ scenario?: string
641
+ ): Promise<MetricsSnapshot | null> {
642
+ let query = this.client
643
+ .from('metrics_history')
644
+ .select('*')
645
+ .eq('date', date)
646
+ .eq('project', project);
647
+
648
+ if (scenario) {
649
+ query = query.eq('scenario', scenario);
650
+ } else {
651
+ query = query.is('scenario', null);
652
+ }
653
+
654
+ const { data, error } = await query.single();
655
+
656
+ if (error || !data) {
657
+ return null;
658
+ }
659
+
660
+ return {
661
+ id: data.id,
662
+ date: data.date,
663
+ project: data.project,
664
+ scenario: data.scenario,
665
+ totalRuns: data.total_runs,
666
+ totalCases: data.total_cases,
667
+ passedCases: data.passed_cases,
668
+ failedCases: data.failed_cases,
669
+ avgSuccessRate: data.avg_success_rate,
670
+ avgLatencyMs: data.avg_latency_ms,
671
+ avgTokensPerRun: data.avg_tokens_per_run,
672
+ minSuccessRate: data.min_success_rate,
673
+ maxSuccessRate: data.max_success_rate,
674
+ minLatencyMs: data.min_latency_ms,
675
+ maxLatencyMs: data.max_latency_ms,
676
+ totalTokens: data.total_tokens,
677
+ createdAt: data.created_at,
678
+ updatedAt: data.updated_at,
679
+ };
680
+ }
681
+
682
+ async aggregateDailyMetrics(
683
+ date: string,
684
+ project: string,
685
+ scenario?: string
686
+ ): Promise<MetricsSnapshot> {
687
+ // Query runs for this date
688
+ const startOfDay = `${date}T00:00:00.000Z`;
689
+ const endOfDay = `${date}T23:59:59.999Z`;
690
+
691
+ let query = this.client
692
+ .from('runs')
693
+ .select('*')
694
+ .eq('project', project)
695
+ .gte('started_at', startOfDay)
696
+ .lte('started_at', endOfDay);
697
+
698
+ if (scenario) {
699
+ query = query.eq('scenario', scenario);
700
+ }
701
+
702
+ const { data: runs, error } = await query;
703
+
704
+ if (error) {
705
+ throw new Error(`Failed to aggregate metrics: ${error.message}`);
706
+ }
707
+
708
+ const runList = runs || [];
709
+
710
+ if (runList.length === 0) {
711
+ // Return empty snapshot
712
+ const emptySnapshot: MetricsSnapshot = {
713
+ date,
714
+ project,
715
+ scenario,
716
+ totalRuns: 0,
717
+ totalCases: 0,
718
+ passedCases: 0,
719
+ failedCases: 0,
720
+ avgSuccessRate: 0,
721
+ avgLatencyMs: 0,
722
+ avgTokensPerRun: 0,
723
+ totalTokens: 0,
724
+ };
725
+ await this.saveMetricsSnapshot(emptySnapshot);
726
+ return emptySnapshot;
727
+ }
728
+
729
+ // Aggregate metrics
730
+ const totalRuns = runList.length;
731
+ const totalCases = runList.reduce((sum, r) => sum + r.total_cases, 0);
732
+ const passedCases = runList.reduce((sum, r) => sum + r.passed_cases, 0);
733
+ const failedCases = runList.reduce((sum, r) => sum + r.failed_cases, 0);
734
+ const totalTokens = runList.reduce((sum, r) => sum + r.total_tokens, 0);
735
+
736
+ const successRates = runList.map((r) => r.success_rate);
737
+ const latencies = runList.map((r) => r.median_latency_ms);
738
+
739
+ const snapshot: MetricsSnapshot = {
740
+ date,
741
+ project,
742
+ scenario,
743
+ totalRuns,
744
+ totalCases,
745
+ passedCases,
746
+ failedCases,
747
+ avgSuccessRate: successRates.reduce((a, b) => a + b, 0) / totalRuns,
748
+ avgLatencyMs: latencies.reduce((a, b) => a + b, 0) / totalRuns,
749
+ avgTokensPerRun: totalTokens / totalRuns,
750
+ minSuccessRate: Math.min(...successRates),
751
+ maxSuccessRate: Math.max(...successRates),
752
+ minLatencyMs: Math.min(...latencies),
753
+ maxLatencyMs: Math.max(...latencies),
754
+ totalTokens,
755
+ };
756
+
757
+ await this.saveMetricsSnapshot(snapshot);
758
+ return snapshot;
759
+ }
151
760
  }