@stackbilt/aegis-core 0.1.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 (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,639 @@
1
+ /**
2
+ * ARGUS Correlation Engine + Incident Clustering
3
+ *
4
+ * Pearson correlation with lag detection identifies causal ordering between metrics.
5
+ * Union-find clustering groups related alerts by temporal proximity + correlation.
6
+ */
7
+
8
+ // ─── Types ───────────────────────────────────────────────────
9
+
10
+ export interface CorrelationResult {
11
+ metricA: string;
12
+ metricB: string;
13
+ coefficient: number; // -1 to +1
14
+ lagMinutes: number; // A leads B by N minutes (0 = simultaneous)
15
+ strength: 'strong' | 'moderate' | 'weak' | 'none';
16
+ direction: 'positive' | 'negative' | 'none';
17
+ }
18
+
19
+ export interface MetricSnapshot {
20
+ timestamp: number;
21
+ metrics: Record<string, number>;
22
+ }
23
+
24
+ /** Minimal diagnosis shape needed by the clustering engine */
25
+ export interface ArgusDiagnosis {
26
+ diagnosisId: string;
27
+ tenantId: string;
28
+ stage: string;
29
+ metricName: string;
30
+ severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
31
+ createdAt: number; // epoch ms
32
+ }
33
+
34
+ export interface IncidentCluster {
35
+ incidentId: string;
36
+ tenantId: string;
37
+ stage: string;
38
+ startTime: number;
39
+ endTime: number;
40
+ diagnoses: ArgusDiagnosis[];
41
+ rootCause: RootCause | null;
42
+ correlations: CorrelationResult[];
43
+ severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
44
+ status: 'active' | 'resolved';
45
+ }
46
+
47
+ export interface RootCause {
48
+ metric: string;
49
+ diagnosis: ArgusDiagnosis;
50
+ confidence: number; // 0-1
51
+ reasoning: string;
52
+ }
53
+
54
+ export interface RelatedDiagnosis {
55
+ diagnosis: ArgusDiagnosis;
56
+ relationship: 'same_incident' | 'correlated' | 'temporal';
57
+ correlation?: CorrelationResult;
58
+ timeDeltaMinutes: number;
59
+ }
60
+
61
+ // ─── Constants ───────────────────────────────────────────────
62
+
63
+ const CLUSTERING_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
64
+ const CORRELATION_THRESHOLD = 0.5; // |r| >= 0.5 for clustering
65
+
66
+ const SEVERITY_RANK: Record<string, number> = {
67
+ CRITICAL: 4,
68
+ HIGH: 3,
69
+ MEDIUM: 2,
70
+ LOW: 1,
71
+ };
72
+
73
+ // ─── Correlation Engine ──────────────────────────────────────
74
+
75
+ /**
76
+ * Calculate Pearson correlation coefficient between two arrays
77
+ */
78
+ export function pearsonCorrelation(x: number[], y: number[]): number {
79
+ if (x.length !== y.length || x.length === 0) {
80
+ return 0;
81
+ }
82
+
83
+ const n = x.length;
84
+ const sumX = x.reduce((a, b) => a + b, 0);
85
+ const sumY = y.reduce((a, b) => a + b, 0);
86
+ const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0);
87
+ const sumX2 = x.reduce((acc, xi) => acc + xi * xi, 0);
88
+ const sumY2 = y.reduce((acc, yi) => acc + yi * yi, 0);
89
+
90
+ const numerator = n * sumXY - sumX * sumY;
91
+ const denominator = Math.sqrt(
92
+ (n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)
93
+ );
94
+
95
+ if (denominator === 0) {
96
+ return 0;
97
+ }
98
+
99
+ return numerator / denominator;
100
+ }
101
+
102
+ /**
103
+ * Classify correlation strength
104
+ */
105
+ export function classifyCorrelation(r: number): {
106
+ strength: 'strong' | 'moderate' | 'weak' | 'none';
107
+ direction: 'positive' | 'negative' | 'none';
108
+ } {
109
+ const absR = Math.abs(r);
110
+
111
+ if (absR < 0.3) {
112
+ return { strength: 'none', direction: 'none' };
113
+ }
114
+
115
+ const direction = r > 0 ? 'positive' : 'negative';
116
+
117
+ if (absR >= 0.7) {
118
+ return { strength: 'strong', direction };
119
+ } else if (absR >= 0.5) {
120
+ return { strength: 'moderate', direction };
121
+ } else {
122
+ return { strength: 'weak', direction };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Extract time series for a specific metric from snapshots
128
+ */
129
+ function extractTimeSeries(
130
+ snapshots: MetricSnapshot[],
131
+ metricName: string
132
+ ): { timestamps: number[]; values: number[] } {
133
+ const timestamps: number[] = [];
134
+ const values: number[] = [];
135
+
136
+ for (const snapshot of snapshots) {
137
+ if (metricName in snapshot.metrics) {
138
+ timestamps.push(snapshot.timestamp);
139
+ values.push(snapshot.metrics[metricName]);
140
+ }
141
+ }
142
+
143
+ return { timestamps, values };
144
+ }
145
+
146
+ /**
147
+ * Apply a lag (shift) to a time series
148
+ * Positive lag means the series happened earlier (leads the other)
149
+ */
150
+ function applyLag(values: number[], lagSteps: number): number[] {
151
+ if (lagSteps === 0) {
152
+ return values;
153
+ }
154
+
155
+ if (lagSteps > 0) {
156
+ return values.slice(lagSteps);
157
+ } else {
158
+ return values.slice(0, values.length + lagSteps);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Find optimal lag between two time series that maximizes correlation
164
+ * Returns lag in number of snapshot intervals (typically 5 minutes each)
165
+ */
166
+ export function findOptimalLag(
167
+ valuesA: number[],
168
+ valuesB: number[],
169
+ maxLagSteps: number = 3 // Max 15 minutes at 5-min intervals
170
+ ): { lag: number; coefficient: number } {
171
+ let bestLag = 0;
172
+ let bestCoefficient = 0;
173
+
174
+ for (let lag = -maxLagSteps; lag <= maxLagSteps; lag++) {
175
+ let shiftedA: number[];
176
+ let shiftedB: number[];
177
+
178
+ if (lag >= 0) {
179
+ shiftedA = applyLag(valuesA, lag);
180
+ shiftedB = valuesB.slice(lag);
181
+ } else {
182
+ shiftedA = valuesA.slice(-lag);
183
+ shiftedB = applyLag(valuesB, -lag);
184
+ }
185
+
186
+ const minLen = Math.min(shiftedA.length, shiftedB.length);
187
+ if (minLen < 3) continue;
188
+
189
+ const r = pearsonCorrelation(
190
+ shiftedA.slice(0, minLen),
191
+ shiftedB.slice(0, minLen)
192
+ );
193
+
194
+ if (Math.abs(r) > Math.abs(bestCoefficient)) {
195
+ bestCoefficient = r;
196
+ bestLag = lag;
197
+ }
198
+ }
199
+
200
+ return { lag: bestLag, coefficient: bestCoefficient };
201
+ }
202
+
203
+ /**
204
+ * Calculate correlation between two metrics from snapshot history
205
+ */
206
+ export function calculateCorrelation(
207
+ snapshots: MetricSnapshot[],
208
+ metricA: string,
209
+ metricB: string,
210
+ snapshotIntervalMinutes: number = 5
211
+ ): CorrelationResult {
212
+ const seriesA = extractTimeSeries(snapshots, metricA);
213
+ const seriesB = extractTimeSeries(snapshots, metricB);
214
+
215
+ const commonTimestamps = new Set(
216
+ seriesA.timestamps.filter((t) => seriesB.timestamps.includes(t))
217
+ );
218
+
219
+ if (commonTimestamps.size < 3) {
220
+ return {
221
+ metricA,
222
+ metricB,
223
+ coefficient: 0,
224
+ lagMinutes: 0,
225
+ strength: 'none',
226
+ direction: 'none',
227
+ };
228
+ }
229
+
230
+ const alignedA: number[] = [];
231
+ const alignedB: number[] = [];
232
+
233
+ for (let i = 0; i < seriesA.timestamps.length; i++) {
234
+ if (commonTimestamps.has(seriesA.timestamps[i])) {
235
+ alignedA.push(seriesA.values[i]);
236
+ }
237
+ }
238
+
239
+ for (let i = 0; i < seriesB.timestamps.length; i++) {
240
+ if (commonTimestamps.has(seriesB.timestamps[i])) {
241
+ alignedB.push(seriesB.values[i]);
242
+ }
243
+ }
244
+
245
+ const { lag, coefficient } = findOptimalLag(alignedA, alignedB);
246
+ const lagMinutes = lag * snapshotIntervalMinutes;
247
+ const classification = classifyCorrelation(coefficient);
248
+
249
+ return {
250
+ metricA,
251
+ metricB,
252
+ coefficient: Math.round(coefficient * 1000) / 1000,
253
+ lagMinutes,
254
+ strength: classification.strength,
255
+ direction: classification.direction,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Calculate correlations between all metric pairs in snapshot history
261
+ * Filters to only return meaningful correlations (|r| >= 0.3)
262
+ */
263
+ export function calculateAllCorrelations(
264
+ snapshots: MetricSnapshot[],
265
+ snapshotIntervalMinutes: number = 5
266
+ ): CorrelationResult[] {
267
+ const metricNames = new Set<string>();
268
+ for (const snapshot of snapshots) {
269
+ for (const metric of Object.keys(snapshot.metrics)) {
270
+ metricNames.add(metric);
271
+ }
272
+ }
273
+
274
+ const metrics = Array.from(metricNames);
275
+ const results: CorrelationResult[] = [];
276
+
277
+ for (let i = 0; i < metrics.length; i++) {
278
+ for (let j = i + 1; j < metrics.length; j++) {
279
+ const correlation = calculateCorrelation(
280
+ snapshots,
281
+ metrics[i],
282
+ metrics[j],
283
+ snapshotIntervalMinutes
284
+ );
285
+
286
+ if (correlation.strength !== 'none') {
287
+ results.push(correlation);
288
+ }
289
+ }
290
+ }
291
+
292
+ results.sort((a, b) => Math.abs(b.coefficient) - Math.abs(a.coefficient));
293
+ return results;
294
+ }
295
+
296
+ /**
297
+ * Find metrics correlated with a specific metric
298
+ */
299
+ export function findCorrelatedMetrics(
300
+ snapshots: MetricSnapshot[],
301
+ targetMetric: string,
302
+ snapshotIntervalMinutes: number = 5
303
+ ): CorrelationResult[] {
304
+ const metricNames = new Set<string>();
305
+ for (const snapshot of snapshots) {
306
+ for (const metric of Object.keys(snapshot.metrics)) {
307
+ if (metric !== targetMetric) {
308
+ metricNames.add(metric);
309
+ }
310
+ }
311
+ }
312
+
313
+ const results: CorrelationResult[] = [];
314
+
315
+ for (const metric of metricNames) {
316
+ const correlation = calculateCorrelation(
317
+ snapshots,
318
+ targetMetric,
319
+ metric,
320
+ snapshotIntervalMinutes
321
+ );
322
+
323
+ if (correlation.strength !== 'none') {
324
+ results.push(correlation);
325
+ }
326
+ }
327
+
328
+ results.sort((a, b) => Math.abs(b.coefficient) - Math.abs(a.coefficient));
329
+ return results;
330
+ }
331
+
332
+ // ─── Incident Clustering ─────────────────────────────────────
333
+
334
+ function getMaxSeverity(
335
+ diagnoses: ArgusDiagnosis[]
336
+ ): 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' {
337
+ let maxRank = 0;
338
+ let maxSeverity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' = 'LOW';
339
+
340
+ for (const d of diagnoses) {
341
+ const rank = SEVERITY_RANK[d.severity] || 0;
342
+ if (rank > maxRank) {
343
+ maxRank = rank;
344
+ maxSeverity = d.severity;
345
+ }
346
+ }
347
+
348
+ return maxSeverity;
349
+ }
350
+
351
+ function areTemporallyClose(d1: ArgusDiagnosis, d2: ArgusDiagnosis): boolean {
352
+ return Math.abs(d1.createdAt - d2.createdAt) <= CLUSTERING_WINDOW_MS;
353
+ }
354
+
355
+ function areCorrelated(
356
+ d1: ArgusDiagnosis,
357
+ d2: ArgusDiagnosis,
358
+ correlations: CorrelationResult[]
359
+ ): CorrelationResult | null {
360
+ for (const corr of correlations) {
361
+ const metrics = [corr.metricA, corr.metricB];
362
+ if (
363
+ metrics.includes(d1.metricName) &&
364
+ metrics.includes(d2.metricName) &&
365
+ Math.abs(corr.coefficient) >= CORRELATION_THRESHOLD
366
+ ) {
367
+ return corr;
368
+ }
369
+ }
370
+ return null;
371
+ }
372
+
373
+ /**
374
+ * Cluster diagnoses into groups based on temporal proximity and correlation.
375
+ * Uses union-find with 15-minute window + |r| >= 0.5 threshold.
376
+ */
377
+ export function clusterDiagnoses(
378
+ diagnoses: ArgusDiagnosis[],
379
+ correlations: CorrelationResult[]
380
+ ): ArgusDiagnosis[][] {
381
+ if (diagnoses.length === 0) return [];
382
+ if (diagnoses.length === 1) return [[diagnoses[0]]];
383
+
384
+ const sorted = [...diagnoses].sort((a, b) => a.createdAt - b.createdAt);
385
+
386
+ // Union-Find
387
+ const parent = new Map<string, string>();
388
+ const rank = new Map<string, number>();
389
+
390
+ function find(id: string): string {
391
+ if (!parent.has(id)) {
392
+ parent.set(id, id);
393
+ rank.set(id, 0);
394
+ }
395
+ if (parent.get(id) !== id) {
396
+ parent.set(id, find(parent.get(id)!));
397
+ }
398
+ return parent.get(id)!;
399
+ }
400
+
401
+ function union(id1: string, id2: string): void {
402
+ const root1 = find(id1);
403
+ const root2 = find(id2);
404
+ if (root1 === root2) return;
405
+
406
+ const rank1 = rank.get(root1) || 0;
407
+ const rank2 = rank.get(root2) || 0;
408
+
409
+ if (rank1 < rank2) {
410
+ parent.set(root1, root2);
411
+ } else if (rank1 > rank2) {
412
+ parent.set(root2, root1);
413
+ } else {
414
+ parent.set(root2, root1);
415
+ rank.set(root1, rank1 + 1);
416
+ }
417
+ }
418
+
419
+ for (const d of sorted) {
420
+ find(d.diagnosisId);
421
+ }
422
+
423
+ for (let i = 0; i < sorted.length; i++) {
424
+ for (let j = i + 1; j < sorted.length; j++) {
425
+ const d1 = sorted[i];
426
+ const d2 = sorted[j];
427
+
428
+ if (d1.tenantId !== d2.tenantId || d1.stage !== d2.stage) {
429
+ continue;
430
+ }
431
+
432
+ if (!areTemporallyClose(d1, d2)) {
433
+ break;
434
+ }
435
+
436
+ if (
437
+ d1.metricName === d2.metricName ||
438
+ areCorrelated(d1, d2, correlations)
439
+ ) {
440
+ union(d1.diagnosisId, d2.diagnosisId);
441
+ }
442
+ }
443
+ }
444
+
445
+ const clusters = new Map<string, ArgusDiagnosis[]>();
446
+ for (const d of sorted) {
447
+ const root = find(d.diagnosisId);
448
+ if (!clusters.has(root)) {
449
+ clusters.set(root, []);
450
+ }
451
+ clusters.get(root)!.push(d);
452
+ }
453
+
454
+ return Array.from(clusters.values());
455
+ }
456
+
457
+ /**
458
+ * Determine root cause from a cluster of diagnoses.
459
+ * Uses earliest-trigger heuristic boosted by lag-correlation evidence.
460
+ */
461
+ export function determineRootCause(
462
+ cluster: ArgusDiagnosis[],
463
+ correlations: CorrelationResult[]
464
+ ): RootCause | null {
465
+ if (cluster.length === 0) return null;
466
+ if (cluster.length === 1) {
467
+ return {
468
+ metric: cluster[0].metricName,
469
+ diagnosis: cluster[0],
470
+ confidence: 1.0,
471
+ reasoning: 'Single diagnosis in incident',
472
+ };
473
+ }
474
+
475
+ const sorted = [...cluster].sort((a, b) => a.createdAt - b.createdAt);
476
+ const earliest = sorted[0];
477
+
478
+ const relevantCorrelations = correlations.filter((c) => {
479
+ const metrics = [c.metricA, c.metricB];
480
+ return (
481
+ metrics.includes(earliest.metricName) &&
482
+ c.lagMinutes !== 0 &&
483
+ Math.abs(c.coefficient) >= CORRELATION_THRESHOLD
484
+ );
485
+ });
486
+
487
+ if (relevantCorrelations.length > 0) {
488
+ const leadsOthers = relevantCorrelations.some((c) => {
489
+ if (c.lagMinutes > 0 && c.metricA === earliest.metricName) return true;
490
+ if (c.lagMinutes < 0 && c.metricB === earliest.metricName) return true;
491
+ return false;
492
+ });
493
+
494
+ if (leadsOthers) {
495
+ const bestCorr = relevantCorrelations.reduce((best, curr) =>
496
+ Math.abs(curr.coefficient) > Math.abs(best.coefficient) ? curr : best
497
+ );
498
+
499
+ return {
500
+ metric: earliest.metricName,
501
+ diagnosis: earliest,
502
+ confidence: Math.abs(bestCorr.coefficient),
503
+ reasoning: `${earliest.metricName} occurred first and has ${bestCorr.strength} correlation with subsequent alerts (r=${bestCorr.coefficient}, leads by ${Math.abs(bestCorr.lagMinutes)} min)`,
504
+ };
505
+ }
506
+ }
507
+
508
+ return {
509
+ metric: earliest.metricName,
510
+ diagnosis: earliest,
511
+ confidence: 0.6,
512
+ reasoning: `${earliest.metricName} was the first metric to trigger an alert in this incident window`,
513
+ };
514
+ }
515
+
516
+ /**
517
+ * Create incidents from diagnoses and correlations.
518
+ * Groups via union-find clustering, then infers root cause per cluster.
519
+ */
520
+ export function createIncidents(
521
+ diagnoses: ArgusDiagnosis[],
522
+ correlations: CorrelationResult[]
523
+ ): IncidentCluster[] {
524
+ const clusters = clusterDiagnoses(diagnoses, correlations);
525
+
526
+ return clusters.map((cluster) => {
527
+ const sorted = [...cluster].sort((a, b) => a.createdAt - b.createdAt);
528
+ const startTime = sorted[0].createdAt;
529
+ const endTime = sorted[sorted.length - 1].createdAt;
530
+
531
+ const clusterMetrics = new Set(cluster.map((d) => d.metricName));
532
+ const relevantCorrelations = correlations.filter((c) => {
533
+ return clusterMetrics.has(c.metricA) && clusterMetrics.has(c.metricB);
534
+ });
535
+
536
+ return {
537
+ incidentId: crypto.randomUUID(),
538
+ tenantId: sorted[0].tenantId,
539
+ stage: sorted[0].stage,
540
+ startTime,
541
+ endTime,
542
+ diagnoses: sorted,
543
+ rootCause: determineRootCause(cluster, correlations),
544
+ correlations: relevantCorrelations,
545
+ severity: getMaxSeverity(cluster),
546
+ status: 'active' as const,
547
+ };
548
+ });
549
+ }
550
+
551
+ /**
552
+ * Find diagnoses related to a specific diagnosis
553
+ */
554
+ export function findRelatedDiagnoses(
555
+ targetDiagnosis: ArgusDiagnosis,
556
+ allDiagnoses: ArgusDiagnosis[],
557
+ correlations: CorrelationResult[],
558
+ snapshots?: MetricSnapshot[]
559
+ ): RelatedDiagnosis[] {
560
+ const related: RelatedDiagnosis[] = [];
561
+ const targetTime = targetDiagnosis.createdAt;
562
+
563
+ const candidates = allDiagnoses.filter(
564
+ (d) =>
565
+ d.diagnosisId !== targetDiagnosis.diagnosisId &&
566
+ d.tenantId === targetDiagnosis.tenantId &&
567
+ d.stage === targetDiagnosis.stage
568
+ );
569
+
570
+ let effectiveCorrelations = correlations;
571
+ if (snapshots && snapshots.length > 0 && correlations.length === 0) {
572
+ effectiveCorrelations = findCorrelatedMetrics(
573
+ snapshots,
574
+ targetDiagnosis.metricName
575
+ );
576
+ }
577
+
578
+ for (const candidate of candidates) {
579
+ const timeDeltaMs = candidate.createdAt - targetTime;
580
+ const timeDeltaMinutes = Math.round(timeDeltaMs / 60000);
581
+
582
+ if (Math.abs(timeDeltaMs) <= CLUSTERING_WINDOW_MS) {
583
+ const correlation = areCorrelated(
584
+ targetDiagnosis,
585
+ candidate,
586
+ effectiveCorrelations
587
+ );
588
+
589
+ if (
590
+ candidate.metricName === targetDiagnosis.metricName ||
591
+ correlation
592
+ ) {
593
+ related.push({
594
+ diagnosis: candidate,
595
+ relationship: 'same_incident',
596
+ correlation: correlation || undefined,
597
+ timeDeltaMinutes,
598
+ });
599
+ continue;
600
+ }
601
+
602
+ related.push({
603
+ diagnosis: candidate,
604
+ relationship: 'temporal',
605
+ timeDeltaMinutes,
606
+ });
607
+ continue;
608
+ }
609
+
610
+ const correlation = areCorrelated(
611
+ targetDiagnosis,
612
+ candidate,
613
+ effectiveCorrelations
614
+ );
615
+ if (correlation) {
616
+ related.push({
617
+ diagnosis: candidate,
618
+ relationship: 'correlated',
619
+ correlation,
620
+ timeDeltaMinutes,
621
+ });
622
+ }
623
+ }
624
+
625
+ const relationshipRank = {
626
+ same_incident: 3,
627
+ correlated: 2,
628
+ temporal: 1,
629
+ };
630
+
631
+ related.sort((a, b) => {
632
+ const rankDiff =
633
+ relationshipRank[b.relationship] - relationshipRank[a.relationship];
634
+ if (rankDiff !== 0) return rankDiff;
635
+ return Math.abs(a.timeDeltaMinutes) - Math.abs(b.timeDeltaMinutes);
636
+ });
637
+
638
+ return related;
639
+ }