@a-company/sentinel 0.2.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/dist/adapters/express.d.ts +44 -0
- package/dist/adapters/express.js +30 -0
- package/dist/adapters/fastify.d.ts +23 -0
- package/dist/adapters/fastify.js +18 -0
- package/dist/adapters/hono.d.ts +23 -0
- package/dist/adapters/hono.js +26 -0
- package/dist/chunk-KPMG4XED.js +1249 -0
- package/dist/cli.js +32 -0
- package/dist/commands-KIMGFR2I.js +3278 -0
- package/dist/dist-2F7NO4H4.js +6851 -0
- package/dist/dist-BPWLYV4U.js +6853 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.js +2270 -0
- package/dist/mcp.js +2767 -0
- package/dist/sdk-B27_vK1g.d.ts +644 -0
- package/dist/server/index.d.ts +82 -0
- package/dist/server/index.js +854 -0
- package/package.json +98 -0
- package/src/seeds/loader.ts +45 -0
- package/src/seeds/paradigm-patterns.json +195 -0
- package/src/seeds/universal-patterns.json +292 -0
- package/ui/dist/assets/index-BNgsn_C8.js +62 -0
- package/ui/dist/assets/index-BNgsn_C8.js.map +1 -0
- package/ui/dist/assets/index-DPxatSdT.css +1 -0
- package/ui/dist/index.html +17 -0
- package/ui/dist/sentinel.svg +19 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SentinelStorage
|
|
3
|
+
} from "./chunk-KPMG4XED.js";
|
|
4
|
+
|
|
5
|
+
// src/matcher.ts
|
|
6
|
+
var DEFAULT_CONFIG = {
|
|
7
|
+
minScore: 30,
|
|
8
|
+
maxResults: 5,
|
|
9
|
+
boostConfidence: true
|
|
10
|
+
};
|
|
11
|
+
var PatternMatcher = class {
|
|
12
|
+
constructor(storage) {
|
|
13
|
+
this.storage = storage;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Match an incident against all patterns and return ranked results
|
|
17
|
+
*/
|
|
18
|
+
match(incident, config = {}) {
|
|
19
|
+
const { minScore, maxResults, boostConfidence } = {
|
|
20
|
+
...DEFAULT_CONFIG,
|
|
21
|
+
...config
|
|
22
|
+
};
|
|
23
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
24
|
+
const matches = [];
|
|
25
|
+
for (const pattern of patterns) {
|
|
26
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const { score, matchedCriteria } = this.scoreMatch(pattern, incident);
|
|
30
|
+
if (score >= minScore) {
|
|
31
|
+
let confidence = score;
|
|
32
|
+
if (boostConfidence) {
|
|
33
|
+
const confidenceFactor = pattern.confidence.score / 100;
|
|
34
|
+
confidence = score * (0.5 + 0.5 * confidenceFactor);
|
|
35
|
+
}
|
|
36
|
+
matches.push({
|
|
37
|
+
pattern,
|
|
38
|
+
score,
|
|
39
|
+
matchedCriteria,
|
|
40
|
+
confidence: Math.round(confidence)
|
|
41
|
+
});
|
|
42
|
+
this.storage.updatePatternConfidence(pattern.id, "matched");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return matches.sort((a, b) => b.confidence - a.confidence).slice(0, maxResults);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Test a pattern against historical incidents
|
|
49
|
+
*/
|
|
50
|
+
testPattern(pattern, limit = 100) {
|
|
51
|
+
const incidents = this.storage.getRecentIncidents({ limit });
|
|
52
|
+
const wouldMatch = [];
|
|
53
|
+
let totalScore = 0;
|
|
54
|
+
for (const incident of incidents) {
|
|
55
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const { score } = this.scoreMatch(pattern, incident);
|
|
59
|
+
if (score >= 30) {
|
|
60
|
+
wouldMatch.push(incident);
|
|
61
|
+
totalScore += score;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
wouldMatch,
|
|
66
|
+
matchCount: wouldMatch.length,
|
|
67
|
+
avgScore: wouldMatch.length > 0 ? Math.round(totalScore / wouldMatch.length) : 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Score how well a pattern matches an incident
|
|
72
|
+
*/
|
|
73
|
+
scoreMatch(pattern, incident) {
|
|
74
|
+
let score = 0;
|
|
75
|
+
const matchedCriteria = {
|
|
76
|
+
symbols: [],
|
|
77
|
+
errorKeywords: [],
|
|
78
|
+
missingSignals: []
|
|
79
|
+
};
|
|
80
|
+
const symbolScore = this.matchSymbols(
|
|
81
|
+
pattern.pattern.symbols,
|
|
82
|
+
incident.symbols,
|
|
83
|
+
matchedCriteria.symbols
|
|
84
|
+
);
|
|
85
|
+
score += Math.min(symbolScore, 50);
|
|
86
|
+
const errorScore = this.matchErrorText(
|
|
87
|
+
pattern,
|
|
88
|
+
incident,
|
|
89
|
+
matchedCriteria.errorKeywords
|
|
90
|
+
);
|
|
91
|
+
score += Math.min(errorScore, 25);
|
|
92
|
+
const signalScore = this.matchMissingSignals(
|
|
93
|
+
pattern,
|
|
94
|
+
incident,
|
|
95
|
+
matchedCriteria.missingSignals
|
|
96
|
+
);
|
|
97
|
+
score += Math.min(signalScore, 25);
|
|
98
|
+
score = Math.min(score, 100);
|
|
99
|
+
return { score, matchedCriteria };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Match symbols between pattern and incident
|
|
103
|
+
*/
|
|
104
|
+
matchSymbols(patternSymbols, incidentSymbols, matched) {
|
|
105
|
+
let score = 0;
|
|
106
|
+
const symbolTypes = [
|
|
107
|
+
"feature",
|
|
108
|
+
"component",
|
|
109
|
+
"flow",
|
|
110
|
+
"gate",
|
|
111
|
+
"signal",
|
|
112
|
+
"state",
|
|
113
|
+
"integration"
|
|
114
|
+
];
|
|
115
|
+
for (const type of symbolTypes) {
|
|
116
|
+
const patternValue = patternSymbols[type];
|
|
117
|
+
const incidentValue = incidentSymbols[type];
|
|
118
|
+
if (!patternValue || !incidentValue) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (typeof patternValue === "string") {
|
|
122
|
+
if (this.matchSingleSymbol(patternValue, incidentValue)) {
|
|
123
|
+
score += patternValue.includes("*") ? 5 : 10;
|
|
124
|
+
matched.push(type);
|
|
125
|
+
}
|
|
126
|
+
} else if (Array.isArray(patternValue)) {
|
|
127
|
+
for (const pv of patternValue) {
|
|
128
|
+
if (this.matchSingleSymbol(pv, incidentValue)) {
|
|
129
|
+
score += 7;
|
|
130
|
+
matched.push(type);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return score;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Match a single symbol value (supports wildcards)
|
|
140
|
+
*/
|
|
141
|
+
matchSingleSymbol(pattern, value) {
|
|
142
|
+
if (pattern === "*") {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (pattern.endsWith("*")) {
|
|
146
|
+
const prefix = pattern.slice(0, -1);
|
|
147
|
+
return value.startsWith(prefix);
|
|
148
|
+
}
|
|
149
|
+
if (pattern.startsWith("*")) {
|
|
150
|
+
const suffix = pattern.slice(1);
|
|
151
|
+
return value.endsWith(suffix);
|
|
152
|
+
}
|
|
153
|
+
if (pattern.includes("*")) {
|
|
154
|
+
const regex = new RegExp(
|
|
155
|
+
"^" + pattern.replace(/\*/g, ".*") + "$"
|
|
156
|
+
);
|
|
157
|
+
return regex.test(value);
|
|
158
|
+
}
|
|
159
|
+
return pattern === value;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Match error text keywords and regex
|
|
163
|
+
*/
|
|
164
|
+
matchErrorText(pattern, incident, matched) {
|
|
165
|
+
let score = 0;
|
|
166
|
+
const errorMessage = incident.error.message.toLowerCase();
|
|
167
|
+
const errorType = incident.error.type?.toLowerCase();
|
|
168
|
+
if (pattern.pattern.errorContains) {
|
|
169
|
+
for (const keyword of pattern.pattern.errorContains) {
|
|
170
|
+
if (errorMessage.includes(keyword.toLowerCase())) {
|
|
171
|
+
score += 5;
|
|
172
|
+
matched.push(keyword);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (pattern.pattern.errorMatches) {
|
|
177
|
+
try {
|
|
178
|
+
const regex = new RegExp(pattern.pattern.errorMatches, "i");
|
|
179
|
+
if (regex.test(incident.error.message)) {
|
|
180
|
+
score += 10;
|
|
181
|
+
matched.push(`regex:${pattern.pattern.errorMatches}`);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (pattern.pattern.errorType && errorType) {
|
|
187
|
+
for (const type of pattern.pattern.errorType) {
|
|
188
|
+
if (errorType.includes(type.toLowerCase())) {
|
|
189
|
+
score += 5;
|
|
190
|
+
matched.push(`type:${type}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return score;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Match missing signals from flow position
|
|
198
|
+
*/
|
|
199
|
+
matchMissingSignals(pattern, incident, matched) {
|
|
200
|
+
if (!pattern.pattern.missingSignals || !incident.flowPosition?.missing) {
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
let score = 0;
|
|
204
|
+
for (const expectedSignal of pattern.pattern.missingSignals) {
|
|
205
|
+
for (const missingSignal of incident.flowPosition.missing) {
|
|
206
|
+
if (this.matchSingleSymbol(expectedSignal, missingSignal)) {
|
|
207
|
+
score += 12;
|
|
208
|
+
matched.push(missingSignal);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return score;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Check if pattern's environment filter matches incident
|
|
217
|
+
*/
|
|
218
|
+
matchEnvironment(pattern, incident) {
|
|
219
|
+
if (!pattern.pattern.environment || pattern.pattern.environment.length === 0) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return pattern.pattern.environment.includes(incident.environment);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/seeds/loader.ts
|
|
227
|
+
import * as path from "path";
|
|
228
|
+
import * as fs from "fs";
|
|
229
|
+
import { fileURLToPath } from "url";
|
|
230
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
231
|
+
var __dirname = path.dirname(__filename);
|
|
232
|
+
function loadUniversalPatterns() {
|
|
233
|
+
const filePath = path.join(__dirname, "universal-patterns.json");
|
|
234
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
235
|
+
return JSON.parse(content);
|
|
236
|
+
}
|
|
237
|
+
function loadParadigmPatterns() {
|
|
238
|
+
const filePath = path.join(__dirname, "paradigm-patterns.json");
|
|
239
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
240
|
+
return JSON.parse(content);
|
|
241
|
+
}
|
|
242
|
+
function loadAllSeedPatterns() {
|
|
243
|
+
const universal = loadUniversalPatterns();
|
|
244
|
+
const paradigm = loadParadigmPatterns();
|
|
245
|
+
return {
|
|
246
|
+
version: "1.0.0",
|
|
247
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
248
|
+
patterns: [...universal.patterns, ...paradigm.patterns]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/sdk.ts
|
|
253
|
+
function ensurePrefix(id, prefix) {
|
|
254
|
+
return id.startsWith(prefix) ? id : `${prefix}${id}`;
|
|
255
|
+
}
|
|
256
|
+
var FlowTracker = class {
|
|
257
|
+
flowId;
|
|
258
|
+
sentinel;
|
|
259
|
+
actual = [];
|
|
260
|
+
expected = [];
|
|
261
|
+
completed = false;
|
|
262
|
+
constructor(flowId, sentinel) {
|
|
263
|
+
this.flowId = ensurePrefix(flowId, "$");
|
|
264
|
+
this.sentinel = sentinel;
|
|
265
|
+
}
|
|
266
|
+
/** Declare which signals/gates are expected in this flow */
|
|
267
|
+
expect(...symbols) {
|
|
268
|
+
this.expected.push(...symbols);
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
/** Record a generic step in the flow */
|
|
272
|
+
step(symbol) {
|
|
273
|
+
this.actual.push(symbol);
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
/** Record a gate check result */
|
|
277
|
+
gate(id, passed) {
|
|
278
|
+
const gateId = ensurePrefix(id, "^");
|
|
279
|
+
this.actual.push(gateId);
|
|
280
|
+
if (!passed) {
|
|
281
|
+
this.fail(new Error(`Gate ${gateId} failed`));
|
|
282
|
+
}
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
/** Record a signal emission */
|
|
286
|
+
signal(id, _data) {
|
|
287
|
+
this.actual.push(ensurePrefix(id, "!"));
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
/** Mark the flow as successfully completed */
|
|
291
|
+
complete() {
|
|
292
|
+
this.completed = true;
|
|
293
|
+
}
|
|
294
|
+
/** Capture an error with full flow position context */
|
|
295
|
+
fail(error) {
|
|
296
|
+
if (this.completed) return;
|
|
297
|
+
this.completed = true;
|
|
298
|
+
const missing = this.expected.filter((s) => !this.actual.includes(s));
|
|
299
|
+
const failedAt = this.actual.length > 0 ? this.actual[this.actual.length - 1] : void 0;
|
|
300
|
+
const flowPosition = {
|
|
301
|
+
flowId: this.flowId,
|
|
302
|
+
expected: this.expected,
|
|
303
|
+
actual: this.actual,
|
|
304
|
+
missing,
|
|
305
|
+
failedAt
|
|
306
|
+
};
|
|
307
|
+
this.sentinel.capture(error, { flow: this.flowId }, flowPosition);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var Sentinel = class {
|
|
311
|
+
storage;
|
|
312
|
+
matcher;
|
|
313
|
+
config;
|
|
314
|
+
ready = false;
|
|
315
|
+
readyPromise = null;
|
|
316
|
+
seeded = false;
|
|
317
|
+
constructor(config) {
|
|
318
|
+
this.config = config;
|
|
319
|
+
this.storage = new SentinelStorage(config.dbPath);
|
|
320
|
+
this.matcher = new PatternMatcher(this.storage);
|
|
321
|
+
}
|
|
322
|
+
/** Explicitly initialize storage. Optional — auto-called on first capture. */
|
|
323
|
+
async init() {
|
|
324
|
+
if (this.ready) return;
|
|
325
|
+
if (this.readyPromise) return this.readyPromise;
|
|
326
|
+
this.readyPromise = this.doInit();
|
|
327
|
+
return this.readyPromise;
|
|
328
|
+
}
|
|
329
|
+
async doInit() {
|
|
330
|
+
await this.storage.ensureReady();
|
|
331
|
+
if (!this.seeded) {
|
|
332
|
+
try {
|
|
333
|
+
const { patterns } = loadAllSeedPatterns();
|
|
334
|
+
for (const pattern of patterns) {
|
|
335
|
+
try {
|
|
336
|
+
this.storage.addPattern(pattern);
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
this.seeded = true;
|
|
343
|
+
}
|
|
344
|
+
this.ready = true;
|
|
345
|
+
}
|
|
346
|
+
ensureReady() {
|
|
347
|
+
if (!this.ready) {
|
|
348
|
+
if (!this.readyPromise) {
|
|
349
|
+
this.readyPromise = this.doInit();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// ── Symbol Context ──────────────────────────────────────────────
|
|
354
|
+
/**
|
|
355
|
+
* Create a component context for scoped error capture.
|
|
356
|
+
*
|
|
357
|
+
* @param id - Component symbol (e.g. '#checkout' or 'checkout')
|
|
358
|
+
* @returns ComponentContext with capture() and wrap() methods
|
|
359
|
+
*/
|
|
360
|
+
component(id) {
|
|
361
|
+
const componentId = ensurePrefix(id, "#");
|
|
362
|
+
const self = this;
|
|
363
|
+
return {
|
|
364
|
+
id: componentId,
|
|
365
|
+
capture(error, extra) {
|
|
366
|
+
return self.capture(error, { component: componentId, ...extra });
|
|
367
|
+
},
|
|
368
|
+
wrap(fn) {
|
|
369
|
+
const wrapped = ((...args) => {
|
|
370
|
+
try {
|
|
371
|
+
const result = fn(...args);
|
|
372
|
+
if (result && typeof result.catch === "function") {
|
|
373
|
+
return result.catch((err) => {
|
|
374
|
+
self.capture(err, { component: componentId });
|
|
375
|
+
throw err;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
} catch (err) {
|
|
380
|
+
if (err instanceof Error) {
|
|
381
|
+
self.capture(err, { component: componentId });
|
|
382
|
+
}
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
return wrapped;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Record a gate check result.
|
|
392
|
+
* If the gate fails, auto-captures an incident.
|
|
393
|
+
*
|
|
394
|
+
* @param id - Gate symbol (e.g. '^authenticated' or 'authenticated')
|
|
395
|
+
* @param passed - Whether the gate passed
|
|
396
|
+
*/
|
|
397
|
+
gate(id, passed) {
|
|
398
|
+
if (!passed) {
|
|
399
|
+
const gateId = ensurePrefix(id, "^");
|
|
400
|
+
this.capture(new Error(`Gate ${gateId} failed`), { gate: gateId });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Record a signal emission. Primarily for flow tracking context.
|
|
405
|
+
*
|
|
406
|
+
* @param id - Signal symbol (e.g. '!payment-authorized' or 'payment-authorized')
|
|
407
|
+
*/
|
|
408
|
+
signal(id, _data) {
|
|
409
|
+
void ensurePrefix(id, "!");
|
|
410
|
+
}
|
|
411
|
+
// ── Flow Tracking ───────────────────────────────────────────────
|
|
412
|
+
/**
|
|
413
|
+
* Create a flow tracker for monitoring multi-step operations.
|
|
414
|
+
*
|
|
415
|
+
* @param id - Flow symbol (e.g. '$checkout-flow' or 'checkout-flow')
|
|
416
|
+
* @returns FlowTracker instance
|
|
417
|
+
*/
|
|
418
|
+
flow(id) {
|
|
419
|
+
return new FlowTracker(id, this);
|
|
420
|
+
}
|
|
421
|
+
// ── Error Capture ───────────────────────────────────────────────
|
|
422
|
+
/**
|
|
423
|
+
* Capture an error with symbolic context.
|
|
424
|
+
*
|
|
425
|
+
* @param error - The error to capture
|
|
426
|
+
* @param context - Symbolic context (component, gate, flow, signal)
|
|
427
|
+
* @param flowPosition - Optional flow position data
|
|
428
|
+
* @returns Incident ID (e.g. 'INC-001')
|
|
429
|
+
*/
|
|
430
|
+
capture(error, context, flowPosition) {
|
|
431
|
+
this.ensureReady();
|
|
432
|
+
const input = {
|
|
433
|
+
error: {
|
|
434
|
+
message: error.message,
|
|
435
|
+
stack: error.stack,
|
|
436
|
+
type: error.constructor.name !== "Error" ? error.constructor.name : void 0
|
|
437
|
+
},
|
|
438
|
+
symbols: context || {},
|
|
439
|
+
environment: this.config.environment || "development",
|
|
440
|
+
service: this.config.service,
|
|
441
|
+
version: this.config.version,
|
|
442
|
+
flowPosition
|
|
443
|
+
};
|
|
444
|
+
const incidentId = this.storage.recordIncident(input);
|
|
445
|
+
const incident = this.storage.getIncident(incidentId);
|
|
446
|
+
if (incident && this.config.onCapture) {
|
|
447
|
+
this.config.onCapture(incident);
|
|
448
|
+
}
|
|
449
|
+
return incidentId;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Get pattern matches for a captured incident.
|
|
453
|
+
*
|
|
454
|
+
* @param incidentId - The incident ID to match
|
|
455
|
+
* @returns Array of pattern matches sorted by confidence
|
|
456
|
+
*/
|
|
457
|
+
match(incidentId) {
|
|
458
|
+
const incident = this.storage.getIncident(incidentId);
|
|
459
|
+
if (!incident) return [];
|
|
460
|
+
return this.matcher.match(incident);
|
|
461
|
+
}
|
|
462
|
+
// ── Framework Integration ───────────────────────────────────────
|
|
463
|
+
/**
|
|
464
|
+
* Create Express error-handling middleware.
|
|
465
|
+
*
|
|
466
|
+
* Usage:
|
|
467
|
+
* app.use(sentinel.express());
|
|
468
|
+
*/
|
|
469
|
+
express() {
|
|
470
|
+
const self = this;
|
|
471
|
+
return (err, req, res, next) => {
|
|
472
|
+
const context = {};
|
|
473
|
+
const routeParts = (req.path || req.url || "").split("/").filter(Boolean);
|
|
474
|
+
if (routeParts.length >= 2) {
|
|
475
|
+
context.component = `#${routeParts[1]}`;
|
|
476
|
+
}
|
|
477
|
+
const incidentId = self.capture(err, context);
|
|
478
|
+
if (res.setHeader) {
|
|
479
|
+
res.setHeader("X-Sentinel-Incident", incidentId);
|
|
480
|
+
}
|
|
481
|
+
next(err);
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
// ── Lifecycle ───────────────────────────────────────────────────
|
|
485
|
+
/** Close the database connection. Call when shutting down. */
|
|
486
|
+
close() {
|
|
487
|
+
this.storage.close();
|
|
488
|
+
this.ready = false;
|
|
489
|
+
this.readyPromise = null;
|
|
490
|
+
}
|
|
491
|
+
/** Get the underlying storage instance (for advanced usage). */
|
|
492
|
+
getStorage() {
|
|
493
|
+
return this.storage;
|
|
494
|
+
}
|
|
495
|
+
/** Get the underlying pattern matcher (for advanced usage). */
|
|
496
|
+
getMatcher() {
|
|
497
|
+
return this.matcher;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/config.ts
|
|
502
|
+
import * as fs2 from "fs";
|
|
503
|
+
import * as path2 from "path";
|
|
504
|
+
var CONFIG_FILES = [".sentinel.yaml", ".sentinel.yml"];
|
|
505
|
+
function loadConfig(projectDir) {
|
|
506
|
+
for (const filename of CONFIG_FILES) {
|
|
507
|
+
const filePath = path2.join(projectDir, filename);
|
|
508
|
+
if (fs2.existsSync(filePath)) {
|
|
509
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
510
|
+
return parseSimpleYaml(content);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
function writeConfig(projectDir, config) {
|
|
516
|
+
const filePath = path2.join(projectDir, ".sentinel.yaml");
|
|
517
|
+
const content = serializeSimpleYaml(config);
|
|
518
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
519
|
+
}
|
|
520
|
+
function parseSimpleYaml(content) {
|
|
521
|
+
const config = { version: "1.0", project: "" };
|
|
522
|
+
const lines = content.split("\n");
|
|
523
|
+
let currentSection = null;
|
|
524
|
+
let currentSubSection = null;
|
|
525
|
+
for (const line of lines) {
|
|
526
|
+
const trimmed = line.trimEnd();
|
|
527
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
528
|
+
const topMatch = trimmed.match(/^(\w+):\s*(.+)$/);
|
|
529
|
+
if (topMatch) {
|
|
530
|
+
const [, key, value] = topMatch;
|
|
531
|
+
if (key === "version") config.version = value.replace(/['"]/g, "");
|
|
532
|
+
else if (key === "project") config.project = value.replace(/['"]/g, "");
|
|
533
|
+
else if (key === "environment") config.environment = value.replace(/['"]/g, "");
|
|
534
|
+
currentSection = null;
|
|
535
|
+
currentSubSection = null;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const sectionMatch = trimmed.match(/^(\w+):$/);
|
|
539
|
+
if (sectionMatch) {
|
|
540
|
+
currentSection = sectionMatch[1];
|
|
541
|
+
currentSubSection = null;
|
|
542
|
+
if (currentSection === "symbols" && !config.symbols) {
|
|
543
|
+
config.symbols = {};
|
|
544
|
+
}
|
|
545
|
+
if (currentSection === "routes" && !config.routes) {
|
|
546
|
+
config.routes = {};
|
|
547
|
+
}
|
|
548
|
+
if (currentSection === "scrub" && !config.scrub) {
|
|
549
|
+
config.scrub = {};
|
|
550
|
+
}
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const subMatch = trimmed.match(/^\s{2}(\w+):$/);
|
|
554
|
+
if (subMatch && currentSection) {
|
|
555
|
+
currentSubSection = subMatch[1];
|
|
556
|
+
if (currentSection === "symbols" && config.symbols) {
|
|
557
|
+
config.symbols[currentSubSection] = [];
|
|
558
|
+
}
|
|
559
|
+
if (currentSection === "scrub" && config.scrub) {
|
|
560
|
+
config.scrub[currentSubSection] = [];
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const listMatch = trimmed.match(/^\s+-\s+(.+)$/);
|
|
565
|
+
if (listMatch && currentSection && currentSubSection) {
|
|
566
|
+
const value = listMatch[1].replace(/['"]/g, "");
|
|
567
|
+
if (currentSection === "symbols" && config.symbols) {
|
|
568
|
+
const arr = config.symbols[currentSubSection];
|
|
569
|
+
if (Array.isArray(arr)) arr.push(value);
|
|
570
|
+
}
|
|
571
|
+
if (currentSection === "scrub" && config.scrub) {
|
|
572
|
+
const arr = config.scrub[currentSubSection];
|
|
573
|
+
if (Array.isArray(arr)) arr.push(value);
|
|
574
|
+
}
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const routeMatch = trimmed.match(/^\s+(['"]?\/[^'"]+['"]?):\s+['"]?([^'"]+)['"]?$/);
|
|
578
|
+
if (routeMatch && currentSection === "routes" && config.routes) {
|
|
579
|
+
const route = routeMatch[1].replace(/['"]/g, "");
|
|
580
|
+
config.routes[route] = routeMatch[2];
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return config;
|
|
585
|
+
}
|
|
586
|
+
function serializeSimpleYaml(config) {
|
|
587
|
+
const lines = [];
|
|
588
|
+
lines.push(`# Sentinel Configuration`);
|
|
589
|
+
lines.push(`# Auto-generated \u2014 edit freely`);
|
|
590
|
+
lines.push("");
|
|
591
|
+
lines.push(`version: "${config.version}"`);
|
|
592
|
+
lines.push(`project: "${config.project}"`);
|
|
593
|
+
if (config.environment) {
|
|
594
|
+
lines.push(`environment: "${config.environment}"`);
|
|
595
|
+
}
|
|
596
|
+
if (config.symbols) {
|
|
597
|
+
lines.push("");
|
|
598
|
+
lines.push("symbols:");
|
|
599
|
+
for (const [key, values] of Object.entries(config.symbols)) {
|
|
600
|
+
if (values && values.length > 0) {
|
|
601
|
+
lines.push(` ${key}:`);
|
|
602
|
+
for (const v of values) {
|
|
603
|
+
lines.push(` - ${v}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (config.routes && Object.keys(config.routes).length > 0) {
|
|
609
|
+
lines.push("");
|
|
610
|
+
lines.push("routes:");
|
|
611
|
+
for (const [route, symbol] of Object.entries(config.routes)) {
|
|
612
|
+
lines.push(` "${route}": ${symbol}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (config.scrub) {
|
|
616
|
+
lines.push("");
|
|
617
|
+
lines.push("scrub:");
|
|
618
|
+
if (config.scrub.headers?.length) {
|
|
619
|
+
lines.push(" headers:");
|
|
620
|
+
for (const h of config.scrub.headers) {
|
|
621
|
+
lines.push(` - ${h}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (config.scrub.fields?.length) {
|
|
625
|
+
lines.push(" fields:");
|
|
626
|
+
for (const f of config.scrub.fields) {
|
|
627
|
+
lines.push(` - ${f}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
lines.push("");
|
|
632
|
+
return lines.join("\n");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/detector.ts
|
|
636
|
+
import * as fs3 from "fs";
|
|
637
|
+
import * as path3 from "path";
|
|
638
|
+
var DIR_PATTERNS = [
|
|
639
|
+
{ dirs: ["services", "src/services"], prefix: "#", type: "components" },
|
|
640
|
+
{ dirs: ["routes", "src/routes", "api", "src/api"], prefix: "#", type: "components" },
|
|
641
|
+
{ dirs: ["handlers", "src/handlers"], prefix: "#", type: "components" },
|
|
642
|
+
{ dirs: ["controllers", "src/controllers"], prefix: "#", type: "components" },
|
|
643
|
+
{ dirs: ["components", "src/components"], prefix: "#", type: "components" },
|
|
644
|
+
{ dirs: ["lib", "src/lib"], prefix: "#", type: "components" },
|
|
645
|
+
{ dirs: ["middleware", "src/middleware"], prefix: "^", type: "gates" },
|
|
646
|
+
{ dirs: ["guards", "src/guards"], prefix: "^", type: "gates" },
|
|
647
|
+
{ dirs: ["auth", "src/auth"], prefix: "^", type: "gates" },
|
|
648
|
+
{ dirs: ["events", "src/events"], prefix: "!", type: "signals" },
|
|
649
|
+
{ dirs: ["listeners", "src/listeners"], prefix: "!", type: "signals" },
|
|
650
|
+
{ dirs: ["flows", "src/flows"], prefix: "$", type: "flows" },
|
|
651
|
+
{ dirs: ["workflows", "src/workflows"], prefix: "$", type: "flows" },
|
|
652
|
+
{ dirs: ["pipelines", "src/pipelines"], prefix: "$", type: "flows" }
|
|
653
|
+
];
|
|
654
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx", ".mjs", ".mts"]);
|
|
655
|
+
function detectSymbols(projectDir) {
|
|
656
|
+
const result = {
|
|
657
|
+
components: [],
|
|
658
|
+
gates: [],
|
|
659
|
+
flows: [],
|
|
660
|
+
signals: [],
|
|
661
|
+
routes: {}
|
|
662
|
+
};
|
|
663
|
+
const purposeSymbols = readPurposeFiles(projectDir);
|
|
664
|
+
if (purposeSymbols) {
|
|
665
|
+
result.components.push(...purposeSymbols.components);
|
|
666
|
+
result.gates.push(...purposeSymbols.gates);
|
|
667
|
+
result.flows.push(...purposeSymbols.flows);
|
|
668
|
+
result.signals.push(...purposeSymbols.signals);
|
|
669
|
+
}
|
|
670
|
+
for (const pattern of DIR_PATTERNS) {
|
|
671
|
+
for (const dir of pattern.dirs) {
|
|
672
|
+
const fullPath = path3.join(projectDir, dir);
|
|
673
|
+
if (!fs3.existsSync(fullPath)) continue;
|
|
674
|
+
const files = safeReaddir(fullPath);
|
|
675
|
+
for (const file of files) {
|
|
676
|
+
const ext = path3.extname(file);
|
|
677
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
678
|
+
const name = path3.basename(file, ext);
|
|
679
|
+
if (name === "index" || name.endsWith(".test") || name.endsWith(".spec")) continue;
|
|
680
|
+
const symbol = `${pattern.prefix}${toKebabCase(name)}`;
|
|
681
|
+
if (!result[pattern.type].includes(symbol)) {
|
|
682
|
+
result[pattern.type].push(symbol);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
scanRoutes(projectDir, result);
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
function generateConfig(projectDir) {
|
|
691
|
+
const detected = detectSymbols(projectDir);
|
|
692
|
+
return {
|
|
693
|
+
version: "1.0",
|
|
694
|
+
project: path3.basename(projectDir),
|
|
695
|
+
symbols: {
|
|
696
|
+
components: detected.components.length > 0 ? detected.components : void 0,
|
|
697
|
+
gates: detected.gates.length > 0 ? detected.gates : void 0,
|
|
698
|
+
flows: detected.flows.length > 0 ? detected.flows : void 0,
|
|
699
|
+
signals: detected.signals.length > 0 ? detected.signals : void 0
|
|
700
|
+
},
|
|
701
|
+
routes: Object.keys(detected.routes).length > 0 ? detected.routes : void 0
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function readPurposeFiles(projectDir) {
|
|
705
|
+
const paradigmDir = path3.join(projectDir, ".paradigm");
|
|
706
|
+
if (!fs3.existsSync(paradigmDir)) return null;
|
|
707
|
+
const result = {
|
|
708
|
+
components: [],
|
|
709
|
+
gates: [],
|
|
710
|
+
flows: [],
|
|
711
|
+
signals: [],
|
|
712
|
+
routes: {}
|
|
713
|
+
};
|
|
714
|
+
const purposeFiles = findFiles(projectDir, ".purpose");
|
|
715
|
+
for (const file of purposeFiles) {
|
|
716
|
+
try {
|
|
717
|
+
const content = fs3.readFileSync(file, "utf-8");
|
|
718
|
+
extractPurposeSymbols(content, result);
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const hasAny = result.components.length > 0 || result.gates.length > 0 || result.flows.length > 0 || result.signals.length > 0;
|
|
723
|
+
return hasAny ? result : null;
|
|
724
|
+
}
|
|
725
|
+
function extractPurposeSymbols(content, result) {
|
|
726
|
+
const lines = content.split("\n");
|
|
727
|
+
let currentSection = "";
|
|
728
|
+
for (const line of lines) {
|
|
729
|
+
const trimmed = line.trim();
|
|
730
|
+
if (trimmed === "components:" || trimmed === "features:") {
|
|
731
|
+
currentSection = "components";
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (trimmed === "gates:") {
|
|
735
|
+
currentSection = "gates";
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (trimmed === "flows:") {
|
|
739
|
+
currentSection = "flows";
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (trimmed === "signals:") {
|
|
743
|
+
currentSection = "signals";
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (currentSection && /^\s{2}\S/.test(line)) {
|
|
747
|
+
const idMatch = trimmed.match(/^([a-zA-Z][\w-]*):$/);
|
|
748
|
+
if (idMatch) {
|
|
749
|
+
const prefixes = {
|
|
750
|
+
components: "#",
|
|
751
|
+
gates: "^",
|
|
752
|
+
flows: "$",
|
|
753
|
+
signals: "!"
|
|
754
|
+
};
|
|
755
|
+
const prefix = prefixes[currentSection] || "#";
|
|
756
|
+
const symbol = `${prefix}${idMatch[1]}`;
|
|
757
|
+
if (!result[currentSection]?.includes(symbol)) {
|
|
758
|
+
result[currentSection]?.push(symbol);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (trimmed && !line.startsWith(" ") && !trimmed.endsWith(":")) {
|
|
763
|
+
currentSection = "";
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function scanRoutes(projectDir, result) {
|
|
768
|
+
const routeDirs = ["routes", "src/routes", "api", "src/api"];
|
|
769
|
+
for (const dir of routeDirs) {
|
|
770
|
+
const fullPath = path3.join(projectDir, dir);
|
|
771
|
+
if (!fs3.existsSync(fullPath)) continue;
|
|
772
|
+
const files = safeReaddir(fullPath);
|
|
773
|
+
for (const file of files) {
|
|
774
|
+
const ext = path3.extname(file);
|
|
775
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
776
|
+
const name = path3.basename(file, ext);
|
|
777
|
+
if (name === "index") continue;
|
|
778
|
+
const routePrefix = `/api/${toKebabCase(name)}`;
|
|
779
|
+
const component = `#${toKebabCase(name)}`;
|
|
780
|
+
result.routes[routePrefix] = component;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function toKebabCase(str) {
|
|
785
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/\..*$/, "").toLowerCase();
|
|
786
|
+
}
|
|
787
|
+
function safeReaddir(dir) {
|
|
788
|
+
try {
|
|
789
|
+
return fs3.readdirSync(dir).filter((f) => {
|
|
790
|
+
const fullPath = path3.join(dir, f);
|
|
791
|
+
try {
|
|
792
|
+
return fs3.statSync(fullPath).isFile();
|
|
793
|
+
} catch {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
} catch {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function findFiles(dir, filename, maxDepth = 4, depth = 0) {
|
|
802
|
+
if (depth > maxDepth) return [];
|
|
803
|
+
const results = [];
|
|
804
|
+
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", "coverage", ".next", ".nuxt"]);
|
|
805
|
+
try {
|
|
806
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
807
|
+
for (const entry of entries) {
|
|
808
|
+
if (entry.isFile() && entry.name === filename) {
|
|
809
|
+
results.push(path3.join(dir, entry.name));
|
|
810
|
+
} else if (entry.isDirectory() && !skipDirs.has(entry.name)) {
|
|
811
|
+
results.push(...findFiles(path3.join(dir, entry.name), filename, maxDepth, depth + 1));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
return results;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/grouper.ts
|
|
820
|
+
var SIMILARITY_THRESHOLD = 0.6;
|
|
821
|
+
var IncidentGrouper = class {
|
|
822
|
+
constructor(storage) {
|
|
823
|
+
this.storage = storage;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Try to find or create a group for an incident
|
|
827
|
+
* Returns the group ID if grouped, null if no suitable group
|
|
828
|
+
*/
|
|
829
|
+
group(incident) {
|
|
830
|
+
const groups = this.storage.getGroups({ limit: 100 });
|
|
831
|
+
for (const group of groups) {
|
|
832
|
+
if (this.shouldJoinGroup(incident, group)) {
|
|
833
|
+
this.storage.addToGroup(group.id, incident.id);
|
|
834
|
+
return group.id;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const similar = this.findSimilar(incident, 10);
|
|
838
|
+
if (similar.length >= 1) {
|
|
839
|
+
const commonSymbols = this.extractCommonSymbols([incident, ...similar]);
|
|
840
|
+
const commonErrorPatterns = this.extractCommonErrorPatterns([
|
|
841
|
+
incident,
|
|
842
|
+
...similar
|
|
843
|
+
]);
|
|
844
|
+
const groupId = this.storage.createGroup({
|
|
845
|
+
incidents: [incident.id, ...similar.map((i) => i.id)],
|
|
846
|
+
commonSymbols,
|
|
847
|
+
commonErrorPatterns,
|
|
848
|
+
firstSeen: this.getEarliestTimestamp([incident, ...similar]),
|
|
849
|
+
lastSeen: incident.timestamp,
|
|
850
|
+
environments: this.getUniqueEnvironments([incident, ...similar])
|
|
851
|
+
});
|
|
852
|
+
return groupId;
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Find incidents similar to the given one
|
|
858
|
+
*/
|
|
859
|
+
findSimilar(incident, limit = 10) {
|
|
860
|
+
const candidates = this.storage.getRecentIncidents({
|
|
861
|
+
limit: 500,
|
|
862
|
+
status: "all"
|
|
863
|
+
});
|
|
864
|
+
const similar = [];
|
|
865
|
+
for (const candidate of candidates) {
|
|
866
|
+
if (candidate.id === incident.id) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const score = this.calculateSimilarity(incident, candidate);
|
|
870
|
+
if (score >= SIMILARITY_THRESHOLD) {
|
|
871
|
+
similar.push({ incident: candidate, score });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return similar.sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.incident);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Analyze ungrouped incidents and create groups automatically
|
|
878
|
+
*/
|
|
879
|
+
analyzeAndGroup(options = {}) {
|
|
880
|
+
const minSize = options.minSize || 3;
|
|
881
|
+
const ungrouped = this.storage.getRecentIncidents({
|
|
882
|
+
limit: 1e3
|
|
883
|
+
}).filter((i) => !i.groupId);
|
|
884
|
+
const newGroups = [];
|
|
885
|
+
const processed = /* @__PURE__ */ new Set();
|
|
886
|
+
for (const incident of ungrouped) {
|
|
887
|
+
if (processed.has(incident.id)) {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const similar = ungrouped.filter(
|
|
891
|
+
(other) => other.id !== incident.id && !processed.has(other.id) && this.calculateSimilarity(incident, other) >= SIMILARITY_THRESHOLD
|
|
892
|
+
);
|
|
893
|
+
if (similar.length + 1 >= minSize) {
|
|
894
|
+
const members = [incident, ...similar];
|
|
895
|
+
const commonSymbols = this.extractCommonSymbols(members);
|
|
896
|
+
const commonErrorPatterns = this.extractCommonErrorPatterns(members);
|
|
897
|
+
const groupId = this.storage.createGroup({
|
|
898
|
+
incidents: members.map((m) => m.id),
|
|
899
|
+
commonSymbols,
|
|
900
|
+
commonErrorPatterns,
|
|
901
|
+
firstSeen: this.getEarliestTimestamp(members),
|
|
902
|
+
lastSeen: this.getLatestTimestamp(members),
|
|
903
|
+
environments: this.getUniqueEnvironments(members)
|
|
904
|
+
});
|
|
905
|
+
for (const m of members) {
|
|
906
|
+
processed.add(m.id);
|
|
907
|
+
}
|
|
908
|
+
const group = this.storage.getGroup(groupId);
|
|
909
|
+
if (group) {
|
|
910
|
+
newGroups.push(group);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return newGroups;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Calculate similarity between two incidents (0-1)
|
|
918
|
+
*/
|
|
919
|
+
calculateSimilarity(a, b) {
|
|
920
|
+
let score = 0;
|
|
921
|
+
let maxScore = 0;
|
|
922
|
+
const symbolWeight = 0.6;
|
|
923
|
+
const symbolTypes = [
|
|
924
|
+
"feature",
|
|
925
|
+
"component",
|
|
926
|
+
"flow",
|
|
927
|
+
"gate",
|
|
928
|
+
"signal",
|
|
929
|
+
"state",
|
|
930
|
+
"integration"
|
|
931
|
+
];
|
|
932
|
+
for (const type of symbolTypes) {
|
|
933
|
+
const aValue = a.symbols[type];
|
|
934
|
+
const bValue = b.symbols[type];
|
|
935
|
+
if (aValue || bValue) {
|
|
936
|
+
maxScore += symbolWeight / symbolTypes.length;
|
|
937
|
+
if (aValue === bValue) {
|
|
938
|
+
score += symbolWeight / symbolTypes.length;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const errorWeight = 0.3;
|
|
943
|
+
const errorSimilarity = this.stringSimilarity(
|
|
944
|
+
a.error.message,
|
|
945
|
+
b.error.message
|
|
946
|
+
);
|
|
947
|
+
score += errorWeight * errorSimilarity;
|
|
948
|
+
maxScore += errorWeight;
|
|
949
|
+
const envWeight = 0.1;
|
|
950
|
+
if (a.environment === b.environment) {
|
|
951
|
+
score += envWeight;
|
|
952
|
+
}
|
|
953
|
+
maxScore += envWeight;
|
|
954
|
+
return maxScore > 0 ? score / maxScore : 0;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Calculate string similarity using Levenshtein distance
|
|
958
|
+
*/
|
|
959
|
+
stringSimilarity(a, b) {
|
|
960
|
+
const maxLen = Math.max(a.length, b.length);
|
|
961
|
+
if (maxLen === 0) return 1;
|
|
962
|
+
const distance = this.levenshteinDistance(
|
|
963
|
+
a.toLowerCase(),
|
|
964
|
+
b.toLowerCase()
|
|
965
|
+
);
|
|
966
|
+
return 1 - distance / maxLen;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Levenshtein distance for string comparison
|
|
970
|
+
*/
|
|
971
|
+
levenshteinDistance(a, b) {
|
|
972
|
+
if (a.length === 0) return b.length;
|
|
973
|
+
if (b.length === 0) return a.length;
|
|
974
|
+
const matrix = [];
|
|
975
|
+
for (let i = 0; i <= b.length; i++) {
|
|
976
|
+
matrix[i] = [i];
|
|
977
|
+
}
|
|
978
|
+
for (let j = 0; j <= a.length; j++) {
|
|
979
|
+
matrix[0][j] = j;
|
|
980
|
+
}
|
|
981
|
+
for (let i = 1; i <= b.length; i++) {
|
|
982
|
+
for (let j = 1; j <= a.length; j++) {
|
|
983
|
+
const cost = a[j - 1] === b[i - 1] ? 0 : 1;
|
|
984
|
+
matrix[i][j] = Math.min(
|
|
985
|
+
matrix[i - 1][j] + 1,
|
|
986
|
+
matrix[i][j - 1] + 1,
|
|
987
|
+
matrix[i - 1][j - 1] + cost
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return matrix[b.length][a.length];
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Check if incident should join existing group
|
|
995
|
+
*/
|
|
996
|
+
shouldJoinGroup(incident, group) {
|
|
997
|
+
let matchCount = 0;
|
|
998
|
+
let totalCommon = 0;
|
|
999
|
+
for (const [key, value] of Object.entries(group.commonSymbols)) {
|
|
1000
|
+
if (value) {
|
|
1001
|
+
totalCommon++;
|
|
1002
|
+
const incidentValue = incident.symbols[key];
|
|
1003
|
+
if (incidentValue === value) {
|
|
1004
|
+
matchCount++;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (totalCommon === 0) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
const symbolMatch = matchCount / totalCommon;
|
|
1012
|
+
const errorLower = incident.error.message.toLowerCase();
|
|
1013
|
+
const errorMatch = group.commonErrorPatterns.some(
|
|
1014
|
+
(pattern) => errorLower.includes(pattern.toLowerCase())
|
|
1015
|
+
);
|
|
1016
|
+
return symbolMatch >= 0.5 || errorMatch;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Extract symbols common to all incidents
|
|
1020
|
+
*/
|
|
1021
|
+
extractCommonSymbols(incidents) {
|
|
1022
|
+
if (incidents.length === 0) return {};
|
|
1023
|
+
const first = incidents[0].symbols;
|
|
1024
|
+
const common = {};
|
|
1025
|
+
for (const [key, value] of Object.entries(first)) {
|
|
1026
|
+
if (!value) continue;
|
|
1027
|
+
const allMatch = incidents.every(
|
|
1028
|
+
(i) => i.symbols[key] === value
|
|
1029
|
+
);
|
|
1030
|
+
if (allMatch) {
|
|
1031
|
+
common[key] = value;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return common;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Extract common error patterns from incidents
|
|
1038
|
+
*/
|
|
1039
|
+
extractCommonErrorPatterns(incidents) {
|
|
1040
|
+
if (incidents.length === 0) return [];
|
|
1041
|
+
const wordCounts = /* @__PURE__ */ new Map();
|
|
1042
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
1043
|
+
"the",
|
|
1044
|
+
"a",
|
|
1045
|
+
"an",
|
|
1046
|
+
"is",
|
|
1047
|
+
"are",
|
|
1048
|
+
"was",
|
|
1049
|
+
"were",
|
|
1050
|
+
"in",
|
|
1051
|
+
"on",
|
|
1052
|
+
"at",
|
|
1053
|
+
"to",
|
|
1054
|
+
"for",
|
|
1055
|
+
"of",
|
|
1056
|
+
"with",
|
|
1057
|
+
"error",
|
|
1058
|
+
"failed",
|
|
1059
|
+
"cannot"
|
|
1060
|
+
]);
|
|
1061
|
+
for (const incident of incidents) {
|
|
1062
|
+
const words = incident.error.message.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
1063
|
+
const uniqueWords = new Set(words);
|
|
1064
|
+
for (const word of uniqueWords) {
|
|
1065
|
+
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const threshold = Math.ceil(incidents.length * 0.6);
|
|
1069
|
+
const commonPatterns = Array.from(wordCounts.entries()).filter(([, count]) => count >= threshold).map(([word]) => word).slice(0, 5);
|
|
1070
|
+
return commonPatterns;
|
|
1071
|
+
}
|
|
1072
|
+
getEarliestTimestamp(incidents) {
|
|
1073
|
+
return incidents.reduce(
|
|
1074
|
+
(earliest, i) => i.timestamp < earliest ? i.timestamp : earliest,
|
|
1075
|
+
incidents[0].timestamp
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
getLatestTimestamp(incidents) {
|
|
1079
|
+
return incidents.reduce(
|
|
1080
|
+
(latest, i) => i.timestamp > latest ? i.timestamp : latest,
|
|
1081
|
+
incidents[0].timestamp
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
getUniqueEnvironments(incidents) {
|
|
1085
|
+
return [...new Set(incidents.map((i) => i.environment))];
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
// src/timeline.ts
|
|
1090
|
+
var TimelineBuilder = class {
|
|
1091
|
+
/**
|
|
1092
|
+
* Build a timeline from an incident with flow position
|
|
1093
|
+
*/
|
|
1094
|
+
build(incident) {
|
|
1095
|
+
if (!incident.flowPosition) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const events = [];
|
|
1099
|
+
const baseTime = new Date(incident.timestamp).getTime();
|
|
1100
|
+
events.push({
|
|
1101
|
+
timestamp: new Date(baseTime - 5e3).toISOString(),
|
|
1102
|
+
symbol: incident.flowPosition.flowId,
|
|
1103
|
+
type: "flow-started"
|
|
1104
|
+
});
|
|
1105
|
+
let eventOffset = 1e3;
|
|
1106
|
+
for (const signal of incident.flowPosition.actual) {
|
|
1107
|
+
const type = this.inferEventType(signal);
|
|
1108
|
+
events.push({
|
|
1109
|
+
timestamp: new Date(baseTime - 4e3 + eventOffset).toISOString(),
|
|
1110
|
+
symbol: signal,
|
|
1111
|
+
type
|
|
1112
|
+
});
|
|
1113
|
+
eventOffset += Math.random() * 1e3 + 500;
|
|
1114
|
+
}
|
|
1115
|
+
const failedSymbol = incident.flowPosition.failedAt || incident.flowPosition.missing[0] || incident.symbols.gate || incident.symbols.signal || "unknown";
|
|
1116
|
+
events.push({
|
|
1117
|
+
timestamp: incident.timestamp,
|
|
1118
|
+
symbol: failedSymbol,
|
|
1119
|
+
type: "error",
|
|
1120
|
+
data: {
|
|
1121
|
+
message: incident.error.message,
|
|
1122
|
+
missing: incident.flowPosition.missing
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
return {
|
|
1126
|
+
incidentId: incident.id,
|
|
1127
|
+
flowId: incident.flowPosition.flowId,
|
|
1128
|
+
events,
|
|
1129
|
+
failure: {
|
|
1130
|
+
at: incident.timestamp,
|
|
1131
|
+
symbol: failedSymbol,
|
|
1132
|
+
reason: incident.error.message
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Render timeline as ASCII art
|
|
1138
|
+
*/
|
|
1139
|
+
renderAscii(timeline) {
|
|
1140
|
+
const lines = [];
|
|
1141
|
+
lines.push(`${timeline.flowId} Timeline`);
|
|
1142
|
+
lines.push("\u2550".repeat(40));
|
|
1143
|
+
lines.push("");
|
|
1144
|
+
for (const event of timeline.events) {
|
|
1145
|
+
const time = this.formatTime(event.timestamp);
|
|
1146
|
+
const icon = this.getEventIcon(event.type);
|
|
1147
|
+
const status = this.getEventStatus(event.type);
|
|
1148
|
+
let line = `${time} ${icon} ${event.symbol}`;
|
|
1149
|
+
if (status) {
|
|
1150
|
+
line += ` (${status})`;
|
|
1151
|
+
}
|
|
1152
|
+
lines.push(line);
|
|
1153
|
+
if (event.type === "error" && event.data) {
|
|
1154
|
+
lines.push(` \u2514\u2500 ${event.data.message}`);
|
|
1155
|
+
if (event.data.missing && Array.isArray(event.data.missing) && event.data.missing.length > 0) {
|
|
1156
|
+
lines.push(
|
|
1157
|
+
` \u2514\u2500 Expected: ${event.data.missing.join(", ")}`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const missing = timeline.events.find((e) => e.type === "error")?.data?.missing;
|
|
1163
|
+
if (missing && missing.length > 0) {
|
|
1164
|
+
lines.push("");
|
|
1165
|
+
lines.push(`Missing signals: ${missing.join(", ")}`);
|
|
1166
|
+
}
|
|
1167
|
+
return lines.join("\n");
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Render timeline as structured data (for MCP/JSON output)
|
|
1171
|
+
*/
|
|
1172
|
+
renderStructured(timeline) {
|
|
1173
|
+
return {
|
|
1174
|
+
incidentId: timeline.incidentId,
|
|
1175
|
+
flow: {
|
|
1176
|
+
id: timeline.flowId,
|
|
1177
|
+
eventCount: timeline.events.length
|
|
1178
|
+
},
|
|
1179
|
+
events: timeline.events.map((event) => ({
|
|
1180
|
+
time: this.formatTime(event.timestamp),
|
|
1181
|
+
symbol: event.symbol,
|
|
1182
|
+
type: event.type,
|
|
1183
|
+
status: this.getEventStatus(event.type),
|
|
1184
|
+
data: event.data
|
|
1185
|
+
})),
|
|
1186
|
+
failure: {
|
|
1187
|
+
at: this.formatTime(timeline.failure.at),
|
|
1188
|
+
symbol: timeline.failure.symbol,
|
|
1189
|
+
reason: timeline.failure.reason
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Infer event type from symbol prefix
|
|
1195
|
+
*/
|
|
1196
|
+
inferEventType(symbol) {
|
|
1197
|
+
if (symbol.startsWith("^")) {
|
|
1198
|
+
return "gate-passed";
|
|
1199
|
+
}
|
|
1200
|
+
if (symbol.startsWith("!")) {
|
|
1201
|
+
return "signal-emitted";
|
|
1202
|
+
}
|
|
1203
|
+
if (symbol.startsWith("%")) {
|
|
1204
|
+
return "state-changed";
|
|
1205
|
+
}
|
|
1206
|
+
return "signal-emitted";
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Get icon for event type
|
|
1210
|
+
*/
|
|
1211
|
+
getEventIcon(type) {
|
|
1212
|
+
switch (type) {
|
|
1213
|
+
case "flow-started":
|
|
1214
|
+
return "\u25B6";
|
|
1215
|
+
case "flow-ended":
|
|
1216
|
+
return "\u25A0";
|
|
1217
|
+
case "gate-passed":
|
|
1218
|
+
return "\u2713";
|
|
1219
|
+
case "gate-failed":
|
|
1220
|
+
return "\u2717";
|
|
1221
|
+
case "signal-emitted":
|
|
1222
|
+
return "\u26A1";
|
|
1223
|
+
case "state-changed":
|
|
1224
|
+
return "\u25C6";
|
|
1225
|
+
case "error":
|
|
1226
|
+
return "\u2717";
|
|
1227
|
+
default:
|
|
1228
|
+
return "\u2022";
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Get status text for event type
|
|
1233
|
+
*/
|
|
1234
|
+
getEventStatus(type) {
|
|
1235
|
+
switch (type) {
|
|
1236
|
+
case "gate-passed":
|
|
1237
|
+
return "PASSED";
|
|
1238
|
+
case "gate-failed":
|
|
1239
|
+
return "FAILED";
|
|
1240
|
+
case "signal-emitted":
|
|
1241
|
+
return "EMITTED";
|
|
1242
|
+
case "state-changed":
|
|
1243
|
+
return "CHANGED";
|
|
1244
|
+
case "error":
|
|
1245
|
+
return "ERROR";
|
|
1246
|
+
default:
|
|
1247
|
+
return "";
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Format timestamp for display
|
|
1252
|
+
*/
|
|
1253
|
+
formatTime(timestamp) {
|
|
1254
|
+
const date = new Date(timestamp);
|
|
1255
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
1256
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
1257
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
1258
|
+
const millis = String(date.getMilliseconds()).padStart(3, "0");
|
|
1259
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
// src/stats.ts
|
|
1264
|
+
var StatsCalculator = class {
|
|
1265
|
+
constructor(storage) {
|
|
1266
|
+
this.storage = storage;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Get comprehensive statistics for a time period
|
|
1270
|
+
*/
|
|
1271
|
+
getStats(periodDays = 7) {
|
|
1272
|
+
const end = (/* @__PURE__ */ new Date()).toISOString();
|
|
1273
|
+
const start = new Date(
|
|
1274
|
+
Date.now() - periodDays * 24 * 60 * 60 * 1e3
|
|
1275
|
+
).toISOString();
|
|
1276
|
+
return this.storage.getStats({ start, end });
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Get health metrics for a specific symbol
|
|
1280
|
+
*/
|
|
1281
|
+
getSymbolHealth(symbol) {
|
|
1282
|
+
return this.storage.getSymbolHealth(symbol);
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Get trending issues (symbols with increasing incident rates)
|
|
1286
|
+
*/
|
|
1287
|
+
getTrendingIssues(days = 7) {
|
|
1288
|
+
const now = Date.now();
|
|
1289
|
+
const halfPeriod = days * 24 * 60 * 60 * 1e3 / 2;
|
|
1290
|
+
const firstHalfStart = new Date(now - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
1291
|
+
const midpoint = new Date(now - halfPeriod).toISOString();
|
|
1292
|
+
const secondHalfEnd = new Date(now).toISOString();
|
|
1293
|
+
const firstHalfIncidents = this.storage.getRecentIncidents({
|
|
1294
|
+
dateFrom: firstHalfStart,
|
|
1295
|
+
dateTo: midpoint,
|
|
1296
|
+
limit: 1e3
|
|
1297
|
+
});
|
|
1298
|
+
const secondHalfIncidents = this.storage.getRecentIncidents({
|
|
1299
|
+
dateFrom: midpoint,
|
|
1300
|
+
dateTo: secondHalfEnd,
|
|
1301
|
+
limit: 1e3
|
|
1302
|
+
});
|
|
1303
|
+
const firstHalfCounts = this.countSymbols(firstHalfIncidents);
|
|
1304
|
+
const secondHalfCounts = this.countSymbols(secondHalfIncidents);
|
|
1305
|
+
const trends = [];
|
|
1306
|
+
const allSymbols = /* @__PURE__ */ new Set([
|
|
1307
|
+
...firstHalfCounts.keys(),
|
|
1308
|
+
...secondHalfCounts.keys()
|
|
1309
|
+
]);
|
|
1310
|
+
for (const symbol of allSymbols) {
|
|
1311
|
+
const first = firstHalfCounts.get(symbol) || 0;
|
|
1312
|
+
const second = secondHalfCounts.get(symbol) || 0;
|
|
1313
|
+
if (first === 0 && second > 0) {
|
|
1314
|
+
trends.push({ symbol, trend: second * 100 });
|
|
1315
|
+
} else if (first > 0) {
|
|
1316
|
+
const change = (second - first) / first * 100;
|
|
1317
|
+
trends.push({ symbol, trend: change });
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return trends.filter((t) => t.trend > 0).sort((a, b) => b.trend - a.trend).slice(0, 10);
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Get resolution metrics
|
|
1324
|
+
*/
|
|
1325
|
+
getResolutionMetrics() {
|
|
1326
|
+
const stats = this.getStats(30);
|
|
1327
|
+
return {
|
|
1328
|
+
avgTimeToResolve: stats.resolution.avgTimeToResolve,
|
|
1329
|
+
resolvedWithPattern: stats.resolution.resolvedWithPattern,
|
|
1330
|
+
resolvedManually: stats.resolution.resolvedManually,
|
|
1331
|
+
totalResolved: stats.incidents.resolved,
|
|
1332
|
+
resolutionRate: stats.resolution.resolutionRate
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Get pattern effectiveness metrics
|
|
1337
|
+
*/
|
|
1338
|
+
getPatternEffectiveness() {
|
|
1339
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
1340
|
+
return patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
|
|
1341
|
+
patternId: p.id,
|
|
1342
|
+
name: p.name,
|
|
1343
|
+
matches: p.confidence.timesMatched,
|
|
1344
|
+
resolutions: p.confidence.timesResolved,
|
|
1345
|
+
recurrences: p.confidence.timesRecurred,
|
|
1346
|
+
effectiveness: p.confidence.timesMatched > 0 ? Math.round(
|
|
1347
|
+
(p.confidence.timesResolved - p.confidence.timesRecurred) / p.confidence.timesMatched * 100
|
|
1348
|
+
) : 0
|
|
1349
|
+
})).sort((a, b) => b.effectiveness - a.effectiveness);
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Get incident rate by hour of day
|
|
1353
|
+
*/
|
|
1354
|
+
getIncidentsByHour(days = 7) {
|
|
1355
|
+
const start = new Date(
|
|
1356
|
+
Date.now() - days * 24 * 60 * 60 * 1e3
|
|
1357
|
+
).toISOString();
|
|
1358
|
+
const incidents = this.storage.getRecentIncidents({
|
|
1359
|
+
dateFrom: start,
|
|
1360
|
+
limit: 1e4
|
|
1361
|
+
});
|
|
1362
|
+
const hourCounts = /* @__PURE__ */ new Map();
|
|
1363
|
+
for (let i = 0; i < 24; i++) {
|
|
1364
|
+
hourCounts.set(i, 0);
|
|
1365
|
+
}
|
|
1366
|
+
for (const incident of incidents) {
|
|
1367
|
+
const hour = new Date(incident.timestamp).getHours();
|
|
1368
|
+
hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
|
|
1369
|
+
}
|
|
1370
|
+
return Array.from(hourCounts.entries()).map(([hour, count]) => ({
|
|
1371
|
+
hour,
|
|
1372
|
+
count
|
|
1373
|
+
}));
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Get incident rate by environment
|
|
1377
|
+
*/
|
|
1378
|
+
getIncidentsByEnvironment() {
|
|
1379
|
+
const stats = this.getStats(30);
|
|
1380
|
+
const total = stats.incidents.total;
|
|
1381
|
+
return Object.entries(stats.incidents.byEnvironment).map(([environment, count]) => ({
|
|
1382
|
+
environment,
|
|
1383
|
+
count,
|
|
1384
|
+
percentage: total > 0 ? Math.round(count / total * 100) : 0
|
|
1385
|
+
})).sort((a, b) => b.count - a.count);
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Get symbol correlation matrix (which symbols fail together)
|
|
1389
|
+
*/
|
|
1390
|
+
getSymbolCorrelation() {
|
|
1391
|
+
const incidents = this.storage.getRecentIncidents({ limit: 1e3 });
|
|
1392
|
+
const correlations = /* @__PURE__ */ new Map();
|
|
1393
|
+
const symbolCounts = /* @__PURE__ */ new Map();
|
|
1394
|
+
for (const incident of incidents) {
|
|
1395
|
+
const symbols = this.getSymbolsFromIncident(incident);
|
|
1396
|
+
for (const symbol of symbols) {
|
|
1397
|
+
symbolCounts.set(symbol, (symbolCounts.get(symbol) || 0) + 1);
|
|
1398
|
+
}
|
|
1399
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1400
|
+
for (let j = i + 1; j < symbols.length; j++) {
|
|
1401
|
+
const key = [symbols[i], symbols[j]].sort().join("|");
|
|
1402
|
+
correlations.set(key, (correlations.get(key) || 0) + 1);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const results = [];
|
|
1407
|
+
for (const [key, count] of correlations) {
|
|
1408
|
+
const [symbol1, symbol2] = key.split("|");
|
|
1409
|
+
const count1 = symbolCounts.get(symbol1) || 1;
|
|
1410
|
+
const count2 = symbolCounts.get(symbol2) || 1;
|
|
1411
|
+
const correlation = count / Math.max(count1, count2);
|
|
1412
|
+
if (correlation > 0.3) {
|
|
1413
|
+
results.push({
|
|
1414
|
+
symbol1,
|
|
1415
|
+
symbol2,
|
|
1416
|
+
correlation: Math.round(correlation * 100) / 100
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return results.sort((a, b) => b.correlation - a.correlation).slice(0, 20);
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Generate a summary dashboard string
|
|
1424
|
+
*/
|
|
1425
|
+
generateDashboard(periodDays = 7) {
|
|
1426
|
+
const stats = this.getStats(periodDays);
|
|
1427
|
+
const lines = [];
|
|
1428
|
+
lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
1429
|
+
lines.push("\u2551 PARADIGM SENTINEL DASHBOARD \u2551");
|
|
1430
|
+
lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
1431
|
+
const todayCount = stats.incidents.byDay[stats.incidents.byDay.length - 1]?.count || 0;
|
|
1432
|
+
lines.push(
|
|
1433
|
+
`\u2551 Open: ${String(stats.incidents.open).padEnd(4)} \u2502 Investigating: ${String(stats.incidents.total - stats.incidents.open - stats.incidents.resolved).padEnd(3)} \u2502 Resolved: ${String(stats.incidents.resolved).padEnd(4)} \u2502 Today: +${todayCount} \u2551`
|
|
1434
|
+
);
|
|
1435
|
+
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
1436
|
+
lines.push("");
|
|
1437
|
+
lines.push("Incidents by Day (last 7 days):");
|
|
1438
|
+
lines.push("\u2500".repeat(50));
|
|
1439
|
+
const maxDayCount = Math.max(...stats.incidents.byDay.map((d) => d.count), 1);
|
|
1440
|
+
for (const day of stats.incidents.byDay.slice(-7)) {
|
|
1441
|
+
const barLength = Math.round(day.count / maxDayCount * 30);
|
|
1442
|
+
const bar = "\u2588".repeat(barLength);
|
|
1443
|
+
lines.push(`${day.date.substring(5)} ${bar} ${day.count}`);
|
|
1444
|
+
}
|
|
1445
|
+
lines.push("");
|
|
1446
|
+
lines.push("Most Affected Symbols:");
|
|
1447
|
+
lines.push("\u2500".repeat(50));
|
|
1448
|
+
for (const { symbol, count } of stats.symbols.mostIncidents.slice(0, 5)) {
|
|
1449
|
+
lines.push(` ${symbol.padEnd(25)} ${count} incidents`);
|
|
1450
|
+
}
|
|
1451
|
+
lines.push("");
|
|
1452
|
+
lines.push("Top Patterns:");
|
|
1453
|
+
lines.push("\u2500".repeat(50));
|
|
1454
|
+
for (const { patternId, resolvedCount } of stats.patterns.mostEffective.slice(0, 5)) {
|
|
1455
|
+
lines.push(` ${patternId.padEnd(25)} ${resolvedCount} resolved`);
|
|
1456
|
+
}
|
|
1457
|
+
lines.push("");
|
|
1458
|
+
lines.push("Resolution Stats:");
|
|
1459
|
+
lines.push("\u2500".repeat(50));
|
|
1460
|
+
lines.push(` Resolution rate: ${Math.round(stats.resolution.resolutionRate)}%`);
|
|
1461
|
+
lines.push(` With pattern: ${stats.resolution.resolvedWithPattern}`);
|
|
1462
|
+
lines.push(` Manual: ${stats.resolution.resolvedManually}`);
|
|
1463
|
+
return lines.join("\n");
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Helper: Count symbols across incidents
|
|
1467
|
+
*/
|
|
1468
|
+
countSymbols(incidents) {
|
|
1469
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1470
|
+
for (const incident of incidents) {
|
|
1471
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
1472
|
+
if (value) {
|
|
1473
|
+
counts.set(value, (counts.get(value) || 0) + 1);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return counts;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Helper: Get all symbols from incident
|
|
1481
|
+
*/
|
|
1482
|
+
getSymbolsFromIncident(incident) {
|
|
1483
|
+
const symbols = [];
|
|
1484
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
1485
|
+
if (value) {
|
|
1486
|
+
symbols.push(value);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return symbols;
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
// src/enricher.ts
|
|
1494
|
+
import * as fs4 from "fs";
|
|
1495
|
+
import * as path4 from "path";
|
|
1496
|
+
var ContextEnricher = class {
|
|
1497
|
+
constructor(projectRoot = process.cwd()) {
|
|
1498
|
+
this.projectRoot = projectRoot;
|
|
1499
|
+
}
|
|
1500
|
+
symbolCache = /* @__PURE__ */ new Map();
|
|
1501
|
+
purposeCache = /* @__PURE__ */ new Map();
|
|
1502
|
+
/**
|
|
1503
|
+
* Enrich an incident with symbol context
|
|
1504
|
+
*/
|
|
1505
|
+
enrich(incident) {
|
|
1506
|
+
const symbolEnrichments = {};
|
|
1507
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
1508
|
+
if (value) {
|
|
1509
|
+
const enrichment = this.getSymbolContext(value);
|
|
1510
|
+
if (enrichment && Object.keys(enrichment).length > 0) {
|
|
1511
|
+
symbolEnrichments[value] = enrichment;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
let flowDescription;
|
|
1516
|
+
if (incident.symbols.flow) {
|
|
1517
|
+
const flowContext = this.getSymbolContext(incident.symbols.flow);
|
|
1518
|
+
flowDescription = flowContext?.description;
|
|
1519
|
+
}
|
|
1520
|
+
return {
|
|
1521
|
+
...incident,
|
|
1522
|
+
enriched: {
|
|
1523
|
+
symbols: symbolEnrichments,
|
|
1524
|
+
flowDescription
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Get symbol metadata from index or .purpose files
|
|
1530
|
+
*/
|
|
1531
|
+
getSymbolContext(symbol) {
|
|
1532
|
+
const cached = this.symbolCache.get(symbol);
|
|
1533
|
+
if (cached) {
|
|
1534
|
+
return cached;
|
|
1535
|
+
}
|
|
1536
|
+
const enrichment = {};
|
|
1537
|
+
const indexEntry = this.findInSymbolIndex(symbol);
|
|
1538
|
+
if (indexEntry) {
|
|
1539
|
+
enrichment.description = indexEntry.description;
|
|
1540
|
+
enrichment.definedIn = indexEntry.file;
|
|
1541
|
+
enrichment.references = indexEntry.references;
|
|
1542
|
+
enrichment.referencedBy = indexEntry.referencedBy;
|
|
1543
|
+
}
|
|
1544
|
+
const purposeEntry = this.findInPurposeFiles(symbol);
|
|
1545
|
+
if (purposeEntry) {
|
|
1546
|
+
if (!enrichment.description && purposeEntry.description) {
|
|
1547
|
+
enrichment.description = purposeEntry.description;
|
|
1548
|
+
}
|
|
1549
|
+
if (purposeEntry.references) {
|
|
1550
|
+
enrichment.references = [
|
|
1551
|
+
.../* @__PURE__ */ new Set([...enrichment.references || [], ...purposeEntry.references])
|
|
1552
|
+
];
|
|
1553
|
+
}
|
|
1554
|
+
if (purposeEntry.referencedBy) {
|
|
1555
|
+
enrichment.referencedBy = [
|
|
1556
|
+
.../* @__PURE__ */ new Set([...enrichment.referencedBy || [], ...purposeEntry.referencedBy])
|
|
1557
|
+
];
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
this.symbolCache.set(symbol, enrichment);
|
|
1561
|
+
return enrichment;
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Find symbol in premise index
|
|
1565
|
+
*/
|
|
1566
|
+
findInSymbolIndex(symbol) {
|
|
1567
|
+
const indexPath = path4.join(this.projectRoot, ".paradigm", "index.json");
|
|
1568
|
+
if (!fs4.existsSync(indexPath)) {
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
try {
|
|
1572
|
+
const indexContent = fs4.readFileSync(indexPath, "utf-8");
|
|
1573
|
+
const index = JSON.parse(indexContent);
|
|
1574
|
+
if (index.symbols && Array.isArray(index.symbols)) {
|
|
1575
|
+
return index.symbols.find(
|
|
1576
|
+
(s) => s.id === symbol
|
|
1577
|
+
) || null;
|
|
1578
|
+
}
|
|
1579
|
+
return null;
|
|
1580
|
+
} catch {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Find symbol in .purpose files
|
|
1586
|
+
*/
|
|
1587
|
+
findInPurposeFiles(symbol) {
|
|
1588
|
+
const searchPaths = this.getSearchPathsForSymbol(symbol);
|
|
1589
|
+
for (const searchPath of searchPaths) {
|
|
1590
|
+
const fullPath = path4.join(this.projectRoot, searchPath);
|
|
1591
|
+
if (!fs4.existsSync(fullPath)) {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const cached = this.purposeCache.get(fullPath);
|
|
1595
|
+
if (cached) {
|
|
1596
|
+
if (cached.symbol === symbol) {
|
|
1597
|
+
return cached;
|
|
1598
|
+
}
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
const content = fs4.readFileSync(fullPath, "utf-8");
|
|
1603
|
+
const purpose = this.parsePurposeFile(content);
|
|
1604
|
+
this.purposeCache.set(fullPath, purpose);
|
|
1605
|
+
if (purpose.symbol === symbol) {
|
|
1606
|
+
return purpose;
|
|
1607
|
+
}
|
|
1608
|
+
} catch {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Get potential file paths for a symbol
|
|
1616
|
+
*/
|
|
1617
|
+
getSearchPathsForSymbol(symbol) {
|
|
1618
|
+
const paths = [];
|
|
1619
|
+
const cleanSymbol = symbol.replace(/^[@#$%^!&~?]/, "");
|
|
1620
|
+
const prefixDirs = {
|
|
1621
|
+
"@": ["features", "src/features"],
|
|
1622
|
+
"#": ["components", "src/components"],
|
|
1623
|
+
"$": ["flows", "src/flows"],
|
|
1624
|
+
"^": ["middleware", "gates", "src/middleware"],
|
|
1625
|
+
"!": ["signals", "events", "src/signals"],
|
|
1626
|
+
"%": ["state", "store", "src/state"],
|
|
1627
|
+
"&": ["integrations", "services", "src/integrations"]
|
|
1628
|
+
};
|
|
1629
|
+
const prefix = symbol[0];
|
|
1630
|
+
const dirs = prefixDirs[prefix] || [];
|
|
1631
|
+
for (const dir of dirs) {
|
|
1632
|
+
paths.push(path4.join(dir, cleanSymbol, ".purpose"));
|
|
1633
|
+
paths.push(path4.join(dir, `${cleanSymbol}.purpose`));
|
|
1634
|
+
}
|
|
1635
|
+
paths.push(path4.join(".paradigm", "purposes", `${cleanSymbol}.yaml`));
|
|
1636
|
+
paths.push(path4.join(".paradigm", "purposes", `${cleanSymbol}.json`));
|
|
1637
|
+
return paths;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Parse a .purpose file
|
|
1641
|
+
*/
|
|
1642
|
+
parsePurposeFile(content) {
|
|
1643
|
+
const result = {};
|
|
1644
|
+
const lines = content.split("\n");
|
|
1645
|
+
for (const line of lines) {
|
|
1646
|
+
const trimmed = line.trim();
|
|
1647
|
+
if (trimmed.startsWith("symbol:")) {
|
|
1648
|
+
result.symbol = trimmed.substring(7).trim();
|
|
1649
|
+
} else if (trimmed.startsWith("description:")) {
|
|
1650
|
+
result.description = trimmed.substring(12).trim();
|
|
1651
|
+
} else if (trimmed.startsWith("purpose:")) {
|
|
1652
|
+
result.description = trimmed.substring(8).trim();
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (!result.description) {
|
|
1656
|
+
const firstLine = lines.find((l) => l.trim() && !l.startsWith("#"));
|
|
1657
|
+
if (firstLine) {
|
|
1658
|
+
result.description = firstLine.trim();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return result;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Clear caches
|
|
1665
|
+
*/
|
|
1666
|
+
clearCache() {
|
|
1667
|
+
this.symbolCache.clear();
|
|
1668
|
+
this.purposeCache.clear();
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Batch enrich multiple incidents
|
|
1672
|
+
*/
|
|
1673
|
+
enrichBatch(incidents) {
|
|
1674
|
+
return incidents.map((i) => this.enrich(i));
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
// src/suggester.ts
|
|
1679
|
+
var PatternSuggester = class {
|
|
1680
|
+
constructor(storage) {
|
|
1681
|
+
this.storage = storage;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Suggest a pattern from a resolved incident
|
|
1685
|
+
*/
|
|
1686
|
+
suggestFromIncident(incident) {
|
|
1687
|
+
const baseId = this.generatePatternId(incident);
|
|
1688
|
+
const symbols = this.buildSymbolCriteria(incident.symbols);
|
|
1689
|
+
const errorKeywords = this.extractErrorKeywords(incident.error.message);
|
|
1690
|
+
const pattern = {
|
|
1691
|
+
id: baseId,
|
|
1692
|
+
name: this.generatePatternName(incident),
|
|
1693
|
+
description: `Auto-suggested pattern from incident ${incident.id}`,
|
|
1694
|
+
pattern: {
|
|
1695
|
+
symbols,
|
|
1696
|
+
errorContains: errorKeywords.length > 0 ? errorKeywords : void 0,
|
|
1697
|
+
missingSignals: incident.flowPosition?.missing
|
|
1698
|
+
},
|
|
1699
|
+
resolution: {
|
|
1700
|
+
description: incident.resolution?.notes || "Resolution approach TBD",
|
|
1701
|
+
strategy: "fix-code",
|
|
1702
|
+
priority: "medium"
|
|
1703
|
+
},
|
|
1704
|
+
source: "suggested",
|
|
1705
|
+
private: false,
|
|
1706
|
+
tags: this.generateTags(incident)
|
|
1707
|
+
};
|
|
1708
|
+
return pattern;
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Suggest a pattern from an incident group
|
|
1712
|
+
*/
|
|
1713
|
+
suggestFromGroup(group) {
|
|
1714
|
+
const baseId = `group-${group.id.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
|
1715
|
+
const symbols = this.buildSymbolCriteria(group.commonSymbols);
|
|
1716
|
+
const pattern = {
|
|
1717
|
+
id: baseId,
|
|
1718
|
+
name: group.name || `Pattern from group ${group.id}`,
|
|
1719
|
+
description: `Auto-suggested pattern from incident group with ${group.count} incidents`,
|
|
1720
|
+
pattern: {
|
|
1721
|
+
symbols,
|
|
1722
|
+
errorContains: group.commonErrorPatterns.length > 0 ? group.commonErrorPatterns : void 0
|
|
1723
|
+
},
|
|
1724
|
+
resolution: {
|
|
1725
|
+
description: "Resolution approach TBD based on grouped incidents",
|
|
1726
|
+
strategy: "fix-code",
|
|
1727
|
+
priority: this.getPriorityFromCount(group.count)
|
|
1728
|
+
},
|
|
1729
|
+
source: "suggested",
|
|
1730
|
+
private: false,
|
|
1731
|
+
tags: this.generateTagsFromGroup(group)
|
|
1732
|
+
};
|
|
1733
|
+
return pattern;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Find incidents that could become patterns
|
|
1737
|
+
*/
|
|
1738
|
+
findPatternCandidates(minOccurrences = 3) {
|
|
1739
|
+
const incidents = this.storage.getRecentIncidents({
|
|
1740
|
+
limit: 1e3,
|
|
1741
|
+
status: "resolved"
|
|
1742
|
+
});
|
|
1743
|
+
const signatureGroups = /* @__PURE__ */ new Map();
|
|
1744
|
+
for (const incident of incidents) {
|
|
1745
|
+
const signature = this.getSymbolSignature(incident.symbols);
|
|
1746
|
+
const existing = signatureGroups.get(signature) || [];
|
|
1747
|
+
existing.push(incident);
|
|
1748
|
+
signatureGroups.set(signature, existing);
|
|
1749
|
+
}
|
|
1750
|
+
const candidates = [];
|
|
1751
|
+
for (const [, groupIncidents] of signatureGroups) {
|
|
1752
|
+
if (groupIncidents.length >= minOccurrences) {
|
|
1753
|
+
const hasPattern = this.hasMatchingPattern(groupIncidents[0]);
|
|
1754
|
+
if (hasPattern) continue;
|
|
1755
|
+
const suggestedPattern = this.suggestFromIncidents(groupIncidents);
|
|
1756
|
+
candidates.push({
|
|
1757
|
+
incidents: groupIncidents,
|
|
1758
|
+
suggestedPattern,
|
|
1759
|
+
occurrenceCount: groupIncidents.length
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return candidates.sort((a, b) => b.occurrenceCount - a.occurrenceCount);
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Generate pattern from multiple similar incidents
|
|
1767
|
+
*/
|
|
1768
|
+
suggestFromIncidents(incidents) {
|
|
1769
|
+
const commonSymbols = this.extractCommonSymbols(incidents);
|
|
1770
|
+
const symbols = this.buildSymbolCriteria(commonSymbols);
|
|
1771
|
+
const errorKeywords = this.extractCommonErrorKeywords(incidents);
|
|
1772
|
+
const missingSignals = this.extractCommonMissingSignals(incidents);
|
|
1773
|
+
const baseId = this.generatePatternId(incidents[0]);
|
|
1774
|
+
return {
|
|
1775
|
+
id: baseId,
|
|
1776
|
+
name: this.generatePatternName(incidents[0]),
|
|
1777
|
+
description: `Auto-suggested pattern from ${incidents.length} similar incidents`,
|
|
1778
|
+
pattern: {
|
|
1779
|
+
symbols,
|
|
1780
|
+
errorContains: errorKeywords.length > 0 ? errorKeywords : void 0,
|
|
1781
|
+
missingSignals: missingSignals.length > 0 ? missingSignals : void 0
|
|
1782
|
+
},
|
|
1783
|
+
resolution: {
|
|
1784
|
+
description: "Resolution approach based on previous resolutions",
|
|
1785
|
+
strategy: this.inferStrategy(incidents),
|
|
1786
|
+
priority: this.getPriorityFromCount(incidents.length)
|
|
1787
|
+
},
|
|
1788
|
+
source: "suggested",
|
|
1789
|
+
private: false,
|
|
1790
|
+
tags: this.generateTagsFromIncidents(incidents)
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Build symbol criteria for pattern, adding wildcards where appropriate
|
|
1795
|
+
*/
|
|
1796
|
+
buildSymbolCriteria(symbols) {
|
|
1797
|
+
const criteria = {};
|
|
1798
|
+
for (const [key, value] of Object.entries(symbols)) {
|
|
1799
|
+
if (value) {
|
|
1800
|
+
criteria[key] = value;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return criteria;
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Extract keywords from error message
|
|
1807
|
+
*/
|
|
1808
|
+
extractErrorKeywords(message) {
|
|
1809
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
1810
|
+
"the",
|
|
1811
|
+
"a",
|
|
1812
|
+
"an",
|
|
1813
|
+
"is",
|
|
1814
|
+
"are",
|
|
1815
|
+
"was",
|
|
1816
|
+
"were",
|
|
1817
|
+
"in",
|
|
1818
|
+
"on",
|
|
1819
|
+
"at",
|
|
1820
|
+
"to",
|
|
1821
|
+
"for",
|
|
1822
|
+
"of",
|
|
1823
|
+
"with",
|
|
1824
|
+
"and",
|
|
1825
|
+
"or",
|
|
1826
|
+
"but",
|
|
1827
|
+
"not",
|
|
1828
|
+
"no",
|
|
1829
|
+
"be",
|
|
1830
|
+
"been",
|
|
1831
|
+
"have",
|
|
1832
|
+
"has",
|
|
1833
|
+
"had",
|
|
1834
|
+
"do",
|
|
1835
|
+
"does",
|
|
1836
|
+
"did"
|
|
1837
|
+
]);
|
|
1838
|
+
const words = message.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
1839
|
+
const unique = [...new Set(words)];
|
|
1840
|
+
return unique.slice(0, 5);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Extract common error keywords from multiple incidents
|
|
1844
|
+
*/
|
|
1845
|
+
extractCommonErrorKeywords(incidents) {
|
|
1846
|
+
const wordCounts = /* @__PURE__ */ new Map();
|
|
1847
|
+
for (const incident of incidents) {
|
|
1848
|
+
const keywords = this.extractErrorKeywords(incident.error.message);
|
|
1849
|
+
for (const keyword of keywords) {
|
|
1850
|
+
wordCounts.set(keyword, (wordCounts.get(keyword) || 0) + 1);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const threshold = Math.ceil(incidents.length * 0.5);
|
|
1854
|
+
return Array.from(wordCounts.entries()).filter(([, count]) => count >= threshold).map(([word]) => word).slice(0, 5);
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Extract symbols common to all incidents
|
|
1858
|
+
*/
|
|
1859
|
+
extractCommonSymbols(incidents) {
|
|
1860
|
+
if (incidents.length === 0) return {};
|
|
1861
|
+
const first = incidents[0].symbols;
|
|
1862
|
+
const common = {};
|
|
1863
|
+
for (const [key, value] of Object.entries(first)) {
|
|
1864
|
+
if (!value) continue;
|
|
1865
|
+
const allMatch = incidents.every(
|
|
1866
|
+
(i) => i.symbols[key] === value
|
|
1867
|
+
);
|
|
1868
|
+
if (allMatch) {
|
|
1869
|
+
common[key] = value;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
return common;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Extract missing signals common to multiple incidents
|
|
1876
|
+
*/
|
|
1877
|
+
extractCommonMissingSignals(incidents) {
|
|
1878
|
+
const signalCounts = /* @__PURE__ */ new Map();
|
|
1879
|
+
for (const incident of incidents) {
|
|
1880
|
+
if (!incident.flowPosition?.missing) continue;
|
|
1881
|
+
for (const signal of incident.flowPosition.missing) {
|
|
1882
|
+
signalCounts.set(signal, (signalCounts.get(signal) || 0) + 1);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const threshold = Math.ceil(incidents.length * 0.5);
|
|
1886
|
+
return Array.from(signalCounts.entries()).filter(([, count]) => count >= threshold).map(([signal]) => signal);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Generate a pattern ID from incident
|
|
1890
|
+
*/
|
|
1891
|
+
generatePatternId(incident) {
|
|
1892
|
+
const parts = [];
|
|
1893
|
+
if (incident.symbols.gate) {
|
|
1894
|
+
parts.push(incident.symbols.gate.replace(/[^a-z0-9]/gi, ""));
|
|
1895
|
+
} else if (incident.symbols.feature) {
|
|
1896
|
+
parts.push(incident.symbols.feature.replace(/[^a-z0-9]/gi, ""));
|
|
1897
|
+
} else if (incident.symbols.component) {
|
|
1898
|
+
parts.push(incident.symbols.component.replace(/[^a-z0-9]/gi, ""));
|
|
1899
|
+
} else if (incident.symbols.integration) {
|
|
1900
|
+
parts.push(incident.symbols.integration.replace(/[^a-z0-9]/gi, ""));
|
|
1901
|
+
} else {
|
|
1902
|
+
parts.push("unknown");
|
|
1903
|
+
}
|
|
1904
|
+
const errorType = incident.error.type?.toLowerCase() || "error";
|
|
1905
|
+
parts.push(errorType.replace(/[^a-z0-9]/gi, ""));
|
|
1906
|
+
parts.push(String(Date.now() % 1e3).padStart(3, "0"));
|
|
1907
|
+
return parts.join("-");
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Generate a human-readable pattern name
|
|
1911
|
+
*/
|
|
1912
|
+
generatePatternName(incident) {
|
|
1913
|
+
const parts = [];
|
|
1914
|
+
if (incident.symbols.feature) {
|
|
1915
|
+
parts.push(
|
|
1916
|
+
incident.symbols.feature.replace("@", "").replace(/-/g, " ")
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
if (incident.symbols.gate) {
|
|
1920
|
+
parts.push("gate " + incident.symbols.gate.replace("^", ""));
|
|
1921
|
+
}
|
|
1922
|
+
if (incident.error.type) {
|
|
1923
|
+
parts.push(incident.error.type);
|
|
1924
|
+
}
|
|
1925
|
+
if (parts.length === 0) {
|
|
1926
|
+
return "Unnamed Pattern";
|
|
1927
|
+
}
|
|
1928
|
+
const name = parts.join(" - ");
|
|
1929
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Generate tags from incident
|
|
1933
|
+
*/
|
|
1934
|
+
generateTags(incident) {
|
|
1935
|
+
const tags = [];
|
|
1936
|
+
if (incident.symbols.feature) {
|
|
1937
|
+
tags.push("feature");
|
|
1938
|
+
}
|
|
1939
|
+
if (incident.symbols.gate) {
|
|
1940
|
+
tags.push("gate");
|
|
1941
|
+
}
|
|
1942
|
+
if (incident.symbols.integration) {
|
|
1943
|
+
tags.push("integration");
|
|
1944
|
+
tags.push(incident.symbols.integration.replace("&", ""));
|
|
1945
|
+
}
|
|
1946
|
+
if (incident.error.type) {
|
|
1947
|
+
tags.push(incident.error.type.toLowerCase());
|
|
1948
|
+
}
|
|
1949
|
+
tags.push(incident.environment);
|
|
1950
|
+
return [...new Set(tags)].slice(0, 5);
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Generate tags from incident group
|
|
1954
|
+
*/
|
|
1955
|
+
generateTagsFromGroup(group) {
|
|
1956
|
+
const tags = ["grouped"];
|
|
1957
|
+
if (group.commonSymbols.feature) {
|
|
1958
|
+
tags.push("feature");
|
|
1959
|
+
}
|
|
1960
|
+
if (group.commonSymbols.gate) {
|
|
1961
|
+
tags.push("gate");
|
|
1962
|
+
}
|
|
1963
|
+
if (group.commonSymbols.integration) {
|
|
1964
|
+
tags.push("integration");
|
|
1965
|
+
}
|
|
1966
|
+
for (const env of group.environments) {
|
|
1967
|
+
tags.push(env);
|
|
1968
|
+
}
|
|
1969
|
+
return [...new Set(tags)].slice(0, 5);
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Generate tags from multiple incidents
|
|
1973
|
+
*/
|
|
1974
|
+
generateTagsFromIncidents(incidents) {
|
|
1975
|
+
const tagCounts = /* @__PURE__ */ new Map();
|
|
1976
|
+
for (const incident of incidents) {
|
|
1977
|
+
const tags = this.generateTags(incident);
|
|
1978
|
+
for (const tag of tags) {
|
|
1979
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return Array.from(tagCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([tag]) => tag);
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Get symbol signature for grouping
|
|
1986
|
+
*/
|
|
1987
|
+
getSymbolSignature(symbols) {
|
|
1988
|
+
const parts = [];
|
|
1989
|
+
if (symbols.feature) parts.push(`f:${symbols.feature}`);
|
|
1990
|
+
if (symbols.component) parts.push(`c:${symbols.component}`);
|
|
1991
|
+
if (symbols.flow) parts.push(`fl:${symbols.flow}`);
|
|
1992
|
+
if (symbols.gate) parts.push(`g:${symbols.gate}`);
|
|
1993
|
+
if (symbols.integration) parts.push(`i:${symbols.integration}`);
|
|
1994
|
+
return parts.sort().join("|");
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Check if there's already a pattern matching this incident
|
|
1998
|
+
*/
|
|
1999
|
+
hasMatchingPattern(incident) {
|
|
2000
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
2001
|
+
for (const pattern of patterns) {
|
|
2002
|
+
let matchCount = 0;
|
|
2003
|
+
const symbolTypes = [
|
|
2004
|
+
"feature",
|
|
2005
|
+
"component",
|
|
2006
|
+
"flow",
|
|
2007
|
+
"gate",
|
|
2008
|
+
"signal",
|
|
2009
|
+
"integration"
|
|
2010
|
+
];
|
|
2011
|
+
for (const type of symbolTypes) {
|
|
2012
|
+
const patternValue = pattern.pattern.symbols[type];
|
|
2013
|
+
const incidentValue = incident.symbols[type];
|
|
2014
|
+
if (patternValue && incidentValue && patternValue === incidentValue) {
|
|
2015
|
+
matchCount++;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (matchCount >= 2) {
|
|
2019
|
+
return true;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Infer resolution strategy from incidents
|
|
2026
|
+
*/
|
|
2027
|
+
inferStrategy(incidents) {
|
|
2028
|
+
const messages = incidents.map((i) => i.error.message.toLowerCase());
|
|
2029
|
+
if (messages.some((m) => m.includes("timeout") || m.includes("network"))) {
|
|
2030
|
+
return "retry";
|
|
2031
|
+
}
|
|
2032
|
+
if (messages.some(
|
|
2033
|
+
(m) => m.includes("validation") || m.includes("invalid") || m.includes("required")
|
|
2034
|
+
)) {
|
|
2035
|
+
return "fix-data";
|
|
2036
|
+
}
|
|
2037
|
+
if (messages.some((m) => m.includes("permission") || m.includes("403"))) {
|
|
2038
|
+
return "escalate";
|
|
2039
|
+
}
|
|
2040
|
+
return "fix-code";
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Get priority based on occurrence count
|
|
2044
|
+
*/
|
|
2045
|
+
getPriorityFromCount(count) {
|
|
2046
|
+
if (count >= 20) return "critical";
|
|
2047
|
+
if (count >= 10) return "high";
|
|
2048
|
+
if (count >= 5) return "medium";
|
|
2049
|
+
return "low";
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
// src/importer.ts
|
|
2054
|
+
import * as fs5 from "fs";
|
|
2055
|
+
var PatternImporter = class {
|
|
2056
|
+
/**
|
|
2057
|
+
* Validate a pattern export file
|
|
2058
|
+
*/
|
|
2059
|
+
validate(data) {
|
|
2060
|
+
const errors = [];
|
|
2061
|
+
const warnings = [];
|
|
2062
|
+
if (!data || typeof data !== "object") {
|
|
2063
|
+
return { valid: false, errors: ["Invalid data: expected object"], warnings: [] };
|
|
2064
|
+
}
|
|
2065
|
+
const obj = data;
|
|
2066
|
+
if (!obj.version) {
|
|
2067
|
+
errors.push("Missing version field");
|
|
2068
|
+
}
|
|
2069
|
+
if (!Array.isArray(obj.patterns)) {
|
|
2070
|
+
errors.push("Missing or invalid patterns array");
|
|
2071
|
+
return { valid: false, errors, warnings };
|
|
2072
|
+
}
|
|
2073
|
+
for (let i = 0; i < obj.patterns.length; i++) {
|
|
2074
|
+
const pattern = obj.patterns[i];
|
|
2075
|
+
const patternErrors = this.validatePattern(pattern, i);
|
|
2076
|
+
errors.push(...patternErrors.errors);
|
|
2077
|
+
warnings.push(...patternErrors.warnings);
|
|
2078
|
+
}
|
|
2079
|
+
return {
|
|
2080
|
+
valid: errors.length === 0,
|
|
2081
|
+
errors,
|
|
2082
|
+
warnings
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Validate a single pattern
|
|
2087
|
+
*/
|
|
2088
|
+
validatePattern(pattern, index) {
|
|
2089
|
+
const errors = [];
|
|
2090
|
+
const warnings = [];
|
|
2091
|
+
const prefix = `Pattern[${index}]`;
|
|
2092
|
+
if (!pattern.id || typeof pattern.id !== "string") {
|
|
2093
|
+
errors.push(`${prefix}: Missing or invalid id`);
|
|
2094
|
+
} else if (!/^[a-z0-9-]+$/.test(pattern.id)) {
|
|
2095
|
+
warnings.push(`${prefix}: ID "${pattern.id}" should be kebab-case`);
|
|
2096
|
+
}
|
|
2097
|
+
if (!pattern.name || typeof pattern.name !== "string") {
|
|
2098
|
+
errors.push(`${prefix}: Missing or invalid name`);
|
|
2099
|
+
}
|
|
2100
|
+
if (!pattern.pattern || typeof pattern.pattern !== "object") {
|
|
2101
|
+
errors.push(`${prefix}: Missing or invalid pattern criteria`);
|
|
2102
|
+
} else {
|
|
2103
|
+
const criteria = pattern.pattern;
|
|
2104
|
+
const hasSymbols = criteria.symbols && typeof criteria.symbols === "object" && Object.keys(criteria.symbols).length > 0;
|
|
2105
|
+
const hasErrorContains = Array.isArray(criteria.errorContains) && criteria.errorContains.length > 0;
|
|
2106
|
+
const hasErrorMatches = criteria.errorMatches && typeof criteria.errorMatches === "string";
|
|
2107
|
+
const hasMissingSignals = Array.isArray(criteria.missingSignals) && criteria.missingSignals.length > 0;
|
|
2108
|
+
if (!hasSymbols && !hasErrorContains && !hasErrorMatches && !hasMissingSignals) {
|
|
2109
|
+
errors.push(`${prefix}: Pattern must have at least one matching criteria`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (!pattern.resolution || typeof pattern.resolution !== "object") {
|
|
2113
|
+
errors.push(`${prefix}: Missing or invalid resolution`);
|
|
2114
|
+
} else {
|
|
2115
|
+
const resolution = pattern.resolution;
|
|
2116
|
+
if (!resolution.description || typeof resolution.description !== "string") {
|
|
2117
|
+
errors.push(`${prefix}: Missing resolution description`);
|
|
2118
|
+
}
|
|
2119
|
+
if (!resolution.strategy || typeof resolution.strategy !== "string") {
|
|
2120
|
+
errors.push(`${prefix}: Missing resolution strategy`);
|
|
2121
|
+
} else {
|
|
2122
|
+
const validStrategies = [
|
|
2123
|
+
"retry",
|
|
2124
|
+
"fallback",
|
|
2125
|
+
"fix-data",
|
|
2126
|
+
"fix-code",
|
|
2127
|
+
"ignore",
|
|
2128
|
+
"escalate"
|
|
2129
|
+
];
|
|
2130
|
+
if (!validStrategies.includes(resolution.strategy)) {
|
|
2131
|
+
errors.push(`${prefix}: Invalid strategy "${resolution.strategy}"`);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
if (!resolution.priority || typeof resolution.priority !== "string") {
|
|
2135
|
+
warnings.push(`${prefix}: Missing priority, will default to medium`);
|
|
2136
|
+
} else {
|
|
2137
|
+
const validPriorities = ["low", "medium", "high", "critical"];
|
|
2138
|
+
if (!validPriorities.includes(resolution.priority)) {
|
|
2139
|
+
warnings.push(`${prefix}: Invalid priority "${resolution.priority}"`);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return { errors, warnings };
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Load patterns from a JSON file
|
|
2147
|
+
*/
|
|
2148
|
+
loadFromFile(filePath) {
|
|
2149
|
+
if (!fs5.existsSync(filePath)) {
|
|
2150
|
+
throw new Error(`File not found: ${filePath}`);
|
|
2151
|
+
}
|
|
2152
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
2153
|
+
const data = JSON.parse(content);
|
|
2154
|
+
const validation = this.validate(data);
|
|
2155
|
+
if (!validation.valid) {
|
|
2156
|
+
throw new Error(`Invalid pattern file: ${validation.errors.join(", ")}`);
|
|
2157
|
+
}
|
|
2158
|
+
return this.normalizeExport(data);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Load patterns from a URL
|
|
2162
|
+
*/
|
|
2163
|
+
async loadFromUrl(url) {
|
|
2164
|
+
const response = await fetch(url);
|
|
2165
|
+
if (!response.ok) {
|
|
2166
|
+
throw new Error(`Failed to fetch patterns: ${response.statusText}`);
|
|
2167
|
+
}
|
|
2168
|
+
const data = await response.json();
|
|
2169
|
+
const validation = this.validate(data);
|
|
2170
|
+
if (!validation.valid) {
|
|
2171
|
+
throw new Error(`Invalid pattern data: ${validation.errors.join(", ")}`);
|
|
2172
|
+
}
|
|
2173
|
+
return this.normalizeExport(data);
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Normalize raw data to PatternExport
|
|
2177
|
+
*/
|
|
2178
|
+
normalizeExport(data) {
|
|
2179
|
+
const patterns = data.patterns.map(
|
|
2180
|
+
(p) => this.normalizePattern(p)
|
|
2181
|
+
);
|
|
2182
|
+
return {
|
|
2183
|
+
version: data.version || "1.0.0",
|
|
2184
|
+
exportedAt: data.exportedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2185
|
+
patterns
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Normalize a raw pattern object
|
|
2190
|
+
*/
|
|
2191
|
+
normalizePattern(data) {
|
|
2192
|
+
const pattern = data.pattern;
|
|
2193
|
+
const resolution = data.resolution;
|
|
2194
|
+
const confidence = data.confidence || {};
|
|
2195
|
+
return {
|
|
2196
|
+
id: data.id,
|
|
2197
|
+
name: data.name,
|
|
2198
|
+
description: data.description || "",
|
|
2199
|
+
pattern: {
|
|
2200
|
+
symbols: pattern.symbols || {},
|
|
2201
|
+
errorContains: pattern.errorContains,
|
|
2202
|
+
errorMatches: pattern.errorMatches,
|
|
2203
|
+
errorType: pattern.errorType,
|
|
2204
|
+
missingSignals: pattern.missingSignals,
|
|
2205
|
+
environment: pattern.environment
|
|
2206
|
+
},
|
|
2207
|
+
resolution: {
|
|
2208
|
+
description: resolution.description,
|
|
2209
|
+
strategy: resolution.strategy,
|
|
2210
|
+
priority: resolution.priority || "medium",
|
|
2211
|
+
codeHint: resolution.codeHint,
|
|
2212
|
+
codeSnippet: resolution.codeSnippet,
|
|
2213
|
+
symbolsToModify: resolution.symbolsToModify,
|
|
2214
|
+
filesLikelyInvolved: resolution.filesLikelyInvolved,
|
|
2215
|
+
commitRef: resolution.commitRef,
|
|
2216
|
+
prRef: resolution.prRef,
|
|
2217
|
+
docsRef: resolution.docsRef
|
|
2218
|
+
},
|
|
2219
|
+
confidence: {
|
|
2220
|
+
score: confidence.score || 50,
|
|
2221
|
+
timesMatched: confidence.timesMatched || 0,
|
|
2222
|
+
timesResolved: confidence.timesResolved || 0,
|
|
2223
|
+
timesRecurred: confidence.timesRecurred || 0,
|
|
2224
|
+
avgTimeToResolve: confidence.avgTimeToResolve,
|
|
2225
|
+
lastMatched: confidence.lastMatched,
|
|
2226
|
+
lastResolved: confidence.lastResolved
|
|
2227
|
+
},
|
|
2228
|
+
source: data.source || "imported",
|
|
2229
|
+
private: Boolean(data.private),
|
|
2230
|
+
tags: data.tags || [],
|
|
2231
|
+
createdAt: data.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2232
|
+
updatedAt: data.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Merge patterns from multiple sources
|
|
2237
|
+
*/
|
|
2238
|
+
mergePatterns(...exports) {
|
|
2239
|
+
const patternMap = /* @__PURE__ */ new Map();
|
|
2240
|
+
for (const exp of exports) {
|
|
2241
|
+
for (const pattern of exp.patterns) {
|
|
2242
|
+
patternMap.set(pattern.id, pattern);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
return {
|
|
2246
|
+
version: "1.0.0",
|
|
2247
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2248
|
+
patterns: Array.from(patternMap.values())
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
export {
|
|
2253
|
+
ContextEnricher,
|
|
2254
|
+
FlowTracker,
|
|
2255
|
+
IncidentGrouper,
|
|
2256
|
+
PatternImporter,
|
|
2257
|
+
PatternMatcher,
|
|
2258
|
+
PatternSuggester,
|
|
2259
|
+
Sentinel,
|
|
2260
|
+
SentinelStorage,
|
|
2261
|
+
StatsCalculator,
|
|
2262
|
+
TimelineBuilder,
|
|
2263
|
+
detectSymbols,
|
|
2264
|
+
generateConfig,
|
|
2265
|
+
loadAllSeedPatterns,
|
|
2266
|
+
loadConfig,
|
|
2267
|
+
loadParadigmPatterns,
|
|
2268
|
+
loadUniversalPatterns,
|
|
2269
|
+
writeConfig
|
|
2270
|
+
};
|