@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.
- package/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- 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
|
+
}
|