@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/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
+ };