@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/mcp.js ADDED
@@ -0,0 +1,2767 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+
11
+ // src/storage.ts
12
+ import initSqlJs from "sql.js";
13
+ import { v4 as uuidv4 } from "uuid";
14
+ import * as path from "path";
15
+ import * as fs from "fs";
16
+ var SCHEMA_VERSION = 2;
17
+ var DEFAULT_CONFIDENCE = {
18
+ score: 50,
19
+ timesMatched: 0,
20
+ timesResolved: 0,
21
+ timesRecurred: 0
22
+ };
23
+ var SQL = null;
24
+ var SentinelStorage = class {
25
+ db = null;
26
+ dbPath;
27
+ incidentCounter = 0;
28
+ initialized = false;
29
+ constructor(dbPath) {
30
+ this.dbPath = dbPath || this.getDefaultDbPath();
31
+ }
32
+ getDefaultDbPath() {
33
+ const dataDir = process.env.SENTINEL_DATA_DIR || process.env.PARADIGM_DATA_DIR || path.join(process.cwd(), ".paradigm", "sentinel");
34
+ return path.join(dataDir, "sentinel.db");
35
+ }
36
+ createSchema() {
37
+ if (!this.db) return;
38
+ this.db.run(`
39
+ -- Metadata table for schema versioning
40
+ CREATE TABLE IF NOT EXISTS metadata (
41
+ key TEXT PRIMARY KEY,
42
+ value TEXT NOT NULL
43
+ );
44
+
45
+ -- Incidents table
46
+ CREATE TABLE IF NOT EXISTS incidents (
47
+ id TEXT PRIMARY KEY,
48
+ timestamp TEXT NOT NULL,
49
+ status TEXT NOT NULL DEFAULT 'open',
50
+ error_message TEXT NOT NULL,
51
+ error_stack TEXT,
52
+ error_code TEXT,
53
+ error_type TEXT,
54
+ symbols TEXT NOT NULL,
55
+ flow_position TEXT,
56
+ environment TEXT NOT NULL,
57
+ service TEXT,
58
+ version TEXT,
59
+ user_id TEXT,
60
+ request_id TEXT,
61
+ group_id TEXT,
62
+ notes TEXT DEFAULT '[]',
63
+ related_incidents TEXT DEFAULT '[]',
64
+ resolved_at TEXT,
65
+ resolved_by TEXT,
66
+ resolution TEXT,
67
+ created_at TEXT NOT NULL,
68
+ updated_at TEXT NOT NULL
69
+ );
70
+
71
+ -- Patterns table
72
+ CREATE TABLE IF NOT EXISTS patterns (
73
+ id TEXT PRIMARY KEY,
74
+ name TEXT NOT NULL,
75
+ description TEXT,
76
+ pattern TEXT NOT NULL,
77
+ resolution TEXT NOT NULL,
78
+ confidence TEXT NOT NULL,
79
+ source TEXT NOT NULL DEFAULT 'manual',
80
+ private INTEGER NOT NULL DEFAULT 0,
81
+ tags TEXT DEFAULT '[]',
82
+ created_at TEXT NOT NULL,
83
+ updated_at TEXT NOT NULL
84
+ );
85
+
86
+ -- Incident groups
87
+ CREATE TABLE IF NOT EXISTS groups (
88
+ id TEXT PRIMARY KEY,
89
+ name TEXT,
90
+ common_symbols TEXT,
91
+ common_error_patterns TEXT,
92
+ suggested_pattern_id TEXT,
93
+ first_seen TEXT NOT NULL,
94
+ last_seen TEXT NOT NULL,
95
+ created_at TEXT NOT NULL,
96
+ updated_at TEXT NOT NULL
97
+ );
98
+
99
+ -- Group members
100
+ CREATE TABLE IF NOT EXISTS group_members (
101
+ group_id TEXT NOT NULL,
102
+ incident_id TEXT NOT NULL,
103
+ added_at TEXT NOT NULL,
104
+ PRIMARY KEY (group_id, incident_id)
105
+ );
106
+
107
+ -- Resolutions history
108
+ CREATE TABLE IF NOT EXISTS resolutions (
109
+ id TEXT PRIMARY KEY,
110
+ incident_id TEXT NOT NULL,
111
+ pattern_id TEXT,
112
+ commit_hash TEXT,
113
+ pr_url TEXT,
114
+ notes TEXT,
115
+ resolved_at TEXT NOT NULL,
116
+ recurred INTEGER DEFAULT 0
117
+ );
118
+
119
+ -- Practice events (habits system)
120
+ CREATE TABLE IF NOT EXISTS practice_events (
121
+ id TEXT PRIMARY KEY,
122
+ timestamp TEXT NOT NULL,
123
+ habit_id TEXT NOT NULL,
124
+ habit_category TEXT NOT NULL,
125
+ result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
126
+ engineer TEXT NOT NULL,
127
+ session_id TEXT NOT NULL,
128
+ lore_entry_id TEXT,
129
+ task_description TEXT,
130
+ symbols_touched TEXT DEFAULT '[]',
131
+ files_modified TEXT DEFAULT '[]',
132
+ related_incident_id TEXT,
133
+ notes TEXT
134
+ );
135
+
136
+ -- Indexes
137
+ CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
138
+ CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
139
+ CREATE INDEX IF NOT EXISTS idx_incidents_environment ON incidents(environment);
140
+ CREATE INDEX IF NOT EXISTS idx_patterns_source ON patterns(source);
141
+ CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
142
+ CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
143
+ CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
144
+ CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
145
+ `);
146
+ this.db.run(
147
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
148
+ [String(SCHEMA_VERSION)]
149
+ );
150
+ }
151
+ save() {
152
+ if (!this.db) return;
153
+ const data = this.db.export();
154
+ const buffer = Buffer.from(data);
155
+ fs.writeFileSync(this.dbPath, buffer);
156
+ }
157
+ // ─── Incidents ───────────────────────────────────────────────────
158
+ recordIncident(input) {
159
+ const db = this.db;
160
+ if (!db) {
161
+ this.initializeSync();
162
+ }
163
+ this.incidentCounter++;
164
+ const id = `INC-${String(this.incidentCounter).padStart(3, "0")}`;
165
+ const now = (/* @__PURE__ */ new Date()).toISOString();
166
+ this.db.run(
167
+ `INSERT INTO incidents (
168
+ id, timestamp, status, error_message, error_stack, error_code, error_type,
169
+ symbols, flow_position, environment, service, version, user_id, request_id,
170
+ group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
171
+ created_at, updated_at
172
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
173
+ [
174
+ id,
175
+ input.timestamp || now,
176
+ input.status || "open",
177
+ input.error.message,
178
+ input.error.stack || null,
179
+ input.error.code || null,
180
+ input.error.type || null,
181
+ JSON.stringify(input.symbols),
182
+ input.flowPosition ? JSON.stringify(input.flowPosition) : null,
183
+ input.environment,
184
+ input.service || null,
185
+ input.version || null,
186
+ input.userId || null,
187
+ input.requestId || null,
188
+ input.groupId || null,
189
+ "[]",
190
+ "[]",
191
+ input.resolvedAt || null,
192
+ input.resolvedBy || null,
193
+ input.resolution ? JSON.stringify(input.resolution) : null,
194
+ now,
195
+ now
196
+ ]
197
+ );
198
+ this.save();
199
+ return id;
200
+ }
201
+ initializeSync() {
202
+ if (this.initialized && this.db) return;
203
+ if (!this.db && SQL) {
204
+ const dir = path.dirname(this.dbPath);
205
+ if (!fs.existsSync(dir)) {
206
+ fs.mkdirSync(dir, { recursive: true });
207
+ }
208
+ if (fs.existsSync(this.dbPath)) {
209
+ const fileData = fs.readFileSync(this.dbPath);
210
+ this.db = new SQL.Database(fileData);
211
+ } else {
212
+ this.db = new SQL.Database();
213
+ this.createSchema();
214
+ }
215
+ try {
216
+ const result = this.db.exec(
217
+ "SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
218
+ );
219
+ if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
220
+ this.incidentCounter = result[0].values[0][0];
221
+ }
222
+ } catch {
223
+ this.incidentCounter = 0;
224
+ }
225
+ this.migrateSchema();
226
+ this.initialized = true;
227
+ this.save();
228
+ }
229
+ }
230
+ /**
231
+ * Run schema migrations from older versions
232
+ */
233
+ migrateSchema() {
234
+ if (!this.db) return;
235
+ let currentVersion = 1;
236
+ try {
237
+ const result = this.db.exec(
238
+ "SELECT value FROM metadata WHERE key = 'schema_version'"
239
+ );
240
+ if (result.length > 0 && result[0].values.length > 0) {
241
+ currentVersion = parseInt(result[0].values[0][0], 10) || 1;
242
+ }
243
+ } catch {
244
+ }
245
+ if (currentVersion < 2) {
246
+ try {
247
+ this.db.run(`
248
+ CREATE TABLE IF NOT EXISTS practice_events (
249
+ id TEXT PRIMARY KEY,
250
+ timestamp TEXT NOT NULL,
251
+ habit_id TEXT NOT NULL,
252
+ habit_category TEXT NOT NULL,
253
+ result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
254
+ engineer TEXT NOT NULL,
255
+ session_id TEXT NOT NULL,
256
+ lore_entry_id TEXT,
257
+ task_description TEXT,
258
+ symbols_touched TEXT DEFAULT '[]',
259
+ files_modified TEXT DEFAULT '[]',
260
+ related_incident_id TEXT,
261
+ notes TEXT
262
+ );
263
+
264
+ CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
265
+ CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
266
+ CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
267
+ CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
268
+ `);
269
+ } catch {
270
+ }
271
+ this.db.run(
272
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
273
+ );
274
+ }
275
+ }
276
+ /**
277
+ * Ensure the storage is ready for use. Must be called once before using storage methods.
278
+ */
279
+ async ensureReady() {
280
+ if (!SQL) {
281
+ SQL = await initSqlJs();
282
+ }
283
+ this.initializeSync();
284
+ }
285
+ getIncident(id) {
286
+ this.initializeSync();
287
+ const result = this.db.exec("SELECT * FROM incidents WHERE id = ?", [id]);
288
+ if (result.length === 0 || result[0].values.length === 0) return null;
289
+ return this.rowToIncident(result[0].columns, result[0].values[0]);
290
+ }
291
+ getRecentIncidents(options = {}) {
292
+ this.initializeSync();
293
+ const { limit = 50, offset = 0 } = options;
294
+ const conditions = [];
295
+ const params = [];
296
+ if (options.status && options.status !== "all") {
297
+ conditions.push("status = ?");
298
+ params.push(options.status);
299
+ }
300
+ if (options.environment) {
301
+ conditions.push("environment = ?");
302
+ params.push(options.environment);
303
+ }
304
+ if (options.symbol) {
305
+ conditions.push("symbols LIKE ?");
306
+ params.push(`%${options.symbol}%`);
307
+ }
308
+ if (options.search) {
309
+ conditions.push("(error_message LIKE ? OR notes LIKE ?)");
310
+ params.push(`%${options.search}%`, `%${options.search}%`);
311
+ }
312
+ if (options.dateFrom) {
313
+ conditions.push("timestamp >= ?");
314
+ params.push(options.dateFrom);
315
+ }
316
+ if (options.dateTo) {
317
+ conditions.push("timestamp <= ?");
318
+ params.push(options.dateTo);
319
+ }
320
+ if (options.groupId) {
321
+ conditions.push("group_id = ?");
322
+ params.push(options.groupId);
323
+ }
324
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
325
+ const result = this.db.exec(
326
+ `SELECT * FROM incidents ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
327
+ [...params, limit, offset]
328
+ );
329
+ if (result.length === 0) return [];
330
+ return result[0].values.map(
331
+ (row) => this.rowToIncident(result[0].columns, row)
332
+ );
333
+ }
334
+ updateIncident(id, updates) {
335
+ this.initializeSync();
336
+ const now = (/* @__PURE__ */ new Date()).toISOString();
337
+ const setClauses = ["updated_at = ?"];
338
+ const params = [now];
339
+ if (updates.status !== void 0) {
340
+ setClauses.push("status = ?");
341
+ params.push(updates.status);
342
+ }
343
+ if (updates.error !== void 0) {
344
+ setClauses.push("error_message = ?");
345
+ params.push(updates.error.message);
346
+ if (updates.error.stack !== void 0) {
347
+ setClauses.push("error_stack = ?");
348
+ params.push(updates.error.stack || null);
349
+ }
350
+ }
351
+ if (updates.symbols !== void 0) {
352
+ setClauses.push("symbols = ?");
353
+ params.push(JSON.stringify(updates.symbols));
354
+ }
355
+ if (updates.flowPosition !== void 0) {
356
+ setClauses.push("flow_position = ?");
357
+ params.push(
358
+ updates.flowPosition ? JSON.stringify(updates.flowPosition) : null
359
+ );
360
+ }
361
+ if (updates.groupId !== void 0) {
362
+ setClauses.push("group_id = ?");
363
+ params.push(updates.groupId || null);
364
+ }
365
+ if (updates.resolvedAt !== void 0) {
366
+ setClauses.push("resolved_at = ?");
367
+ params.push(updates.resolvedAt || null);
368
+ }
369
+ if (updates.resolvedBy !== void 0) {
370
+ setClauses.push("resolved_by = ?");
371
+ params.push(updates.resolvedBy || null);
372
+ }
373
+ if (updates.resolution !== void 0) {
374
+ setClauses.push("resolution = ?");
375
+ params.push(
376
+ updates.resolution ? JSON.stringify(updates.resolution) : null
377
+ );
378
+ }
379
+ params.push(id);
380
+ this.db.run(
381
+ `UPDATE incidents SET ${setClauses.join(", ")} WHERE id = ?`,
382
+ params
383
+ );
384
+ this.save();
385
+ }
386
+ addIncidentNote(incidentId, note) {
387
+ this.initializeSync();
388
+ const incident = this.getIncident(incidentId);
389
+ if (!incident) return;
390
+ const newNote = {
391
+ id: uuidv4(),
392
+ ...note
393
+ };
394
+ const notes = [...incident.notes, newNote];
395
+ this.db.run(
396
+ "UPDATE incidents SET notes = ?, updated_at = ? WHERE id = ?",
397
+ [JSON.stringify(notes), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
398
+ );
399
+ this.save();
400
+ }
401
+ linkIncidents(incidentId, relatedId) {
402
+ this.initializeSync();
403
+ const incident = this.getIncident(incidentId);
404
+ if (!incident) return;
405
+ if (!incident.relatedIncidents.includes(relatedId)) {
406
+ const related = [...incident.relatedIncidents, relatedId];
407
+ this.db.run(
408
+ "UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
409
+ [JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
410
+ );
411
+ }
412
+ const relatedIncident = this.getIncident(relatedId);
413
+ if (relatedIncident && !relatedIncident.relatedIncidents.includes(incidentId)) {
414
+ const related = [...relatedIncident.relatedIncidents, incidentId];
415
+ this.db.run(
416
+ "UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
417
+ [JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), relatedId]
418
+ );
419
+ }
420
+ this.save();
421
+ }
422
+ getIncidentCount(options = {}) {
423
+ this.initializeSync();
424
+ const conditions = [];
425
+ const params = [];
426
+ if (options.status && options.status !== "all") {
427
+ conditions.push("status = ?");
428
+ params.push(options.status);
429
+ }
430
+ if (options.environment) {
431
+ conditions.push("environment = ?");
432
+ params.push(options.environment);
433
+ }
434
+ if (options.dateFrom) {
435
+ conditions.push("timestamp >= ?");
436
+ params.push(options.dateFrom);
437
+ }
438
+ if (options.dateTo) {
439
+ conditions.push("timestamp <= ?");
440
+ params.push(options.dateTo);
441
+ }
442
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
443
+ const result = this.db.exec(
444
+ `SELECT COUNT(*) as count FROM incidents ${whereClause}`,
445
+ params
446
+ );
447
+ if (result.length === 0 || result[0].values.length === 0) return 0;
448
+ return result[0].values[0][0];
449
+ }
450
+ // ─── Patterns ────────────────────────────────────────────────────
451
+ addPattern(input) {
452
+ this.initializeSync();
453
+ const now = (/* @__PURE__ */ new Date()).toISOString();
454
+ const confidence = {
455
+ ...DEFAULT_CONFIDENCE,
456
+ ...input.confidence
457
+ };
458
+ this.db.run(
459
+ `INSERT INTO patterns (
460
+ id, name, description, pattern, resolution, confidence,
461
+ source, private, tags, created_at, updated_at
462
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
463
+ [
464
+ input.id,
465
+ input.name,
466
+ input.description || null,
467
+ JSON.stringify(input.pattern),
468
+ JSON.stringify(input.resolution),
469
+ JSON.stringify(confidence),
470
+ input.source,
471
+ input.private ? 1 : 0,
472
+ JSON.stringify(input.tags || []),
473
+ now,
474
+ now
475
+ ]
476
+ );
477
+ this.save();
478
+ return input.id;
479
+ }
480
+ getPattern(id) {
481
+ this.initializeSync();
482
+ const result = this.db.exec("SELECT * FROM patterns WHERE id = ?", [id]);
483
+ if (result.length === 0 || result[0].values.length === 0) return null;
484
+ return this.rowToPattern(result[0].columns, result[0].values[0]);
485
+ }
486
+ getAllPatterns(options = {}) {
487
+ this.initializeSync();
488
+ const conditions = [];
489
+ const params = [];
490
+ if (options.source) {
491
+ conditions.push("source = ?");
492
+ params.push(options.source);
493
+ }
494
+ if (options.minConfidence !== void 0) {
495
+ conditions.push("json_extract(confidence, '$.score') >= ?");
496
+ params.push(options.minConfidence);
497
+ }
498
+ if (!options.includePrivate) {
499
+ conditions.push("private = 0");
500
+ }
501
+ if (options.tags && options.tags.length > 0) {
502
+ const tagConditions = options.tags.map(() => "tags LIKE ?");
503
+ conditions.push(`(${tagConditions.join(" OR ")})`);
504
+ params.push(...options.tags.map((tag) => `%"${tag}"%`));
505
+ }
506
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
507
+ const result = this.db.exec(
508
+ `SELECT * FROM patterns ${whereClause} ORDER BY json_extract(confidence, '$.score') DESC`,
509
+ params
510
+ );
511
+ if (result.length === 0) return [];
512
+ return result[0].values.map(
513
+ (row) => this.rowToPattern(result[0].columns, row)
514
+ );
515
+ }
516
+ updatePattern(id, updates) {
517
+ this.initializeSync();
518
+ const now = (/* @__PURE__ */ new Date()).toISOString();
519
+ const setClauses = ["updated_at = ?"];
520
+ const params = [now];
521
+ if (updates.name !== void 0) {
522
+ setClauses.push("name = ?");
523
+ params.push(updates.name);
524
+ }
525
+ if (updates.description !== void 0) {
526
+ setClauses.push("description = ?");
527
+ params.push(updates.description || null);
528
+ }
529
+ if (updates.pattern !== void 0) {
530
+ setClauses.push("pattern = ?");
531
+ params.push(JSON.stringify(updates.pattern));
532
+ }
533
+ if (updates.resolution !== void 0) {
534
+ setClauses.push("resolution = ?");
535
+ params.push(JSON.stringify(updates.resolution));
536
+ }
537
+ if (updates.confidence !== void 0) {
538
+ setClauses.push("confidence = ?");
539
+ params.push(JSON.stringify(updates.confidence));
540
+ }
541
+ if (updates.source !== void 0) {
542
+ setClauses.push("source = ?");
543
+ params.push(updates.source);
544
+ }
545
+ if (updates.private !== void 0) {
546
+ setClauses.push("private = ?");
547
+ params.push(updates.private ? 1 : 0);
548
+ }
549
+ if (updates.tags !== void 0) {
550
+ setClauses.push("tags = ?");
551
+ params.push(JSON.stringify(updates.tags));
552
+ }
553
+ params.push(id);
554
+ this.db.run(
555
+ `UPDATE patterns SET ${setClauses.join(", ")} WHERE id = ?`,
556
+ params
557
+ );
558
+ this.save();
559
+ }
560
+ deletePattern(id) {
561
+ this.initializeSync();
562
+ this.db.run("DELETE FROM patterns WHERE id = ?", [id]);
563
+ this.save();
564
+ }
565
+ updatePatternConfidence(patternId, event) {
566
+ const pattern = this.getPattern(patternId);
567
+ if (!pattern) return;
568
+ const now = (/* @__PURE__ */ new Date()).toISOString();
569
+ const confidence = { ...pattern.confidence };
570
+ switch (event) {
571
+ case "matched":
572
+ confidence.timesMatched++;
573
+ confidence.lastMatched = now;
574
+ break;
575
+ case "resolved":
576
+ confidence.timesResolved++;
577
+ confidence.lastResolved = now;
578
+ confidence.score = Math.min(100, confidence.score + 2);
579
+ break;
580
+ case "recurred":
581
+ confidence.timesRecurred++;
582
+ confidence.score = Math.max(10, confidence.score - 5);
583
+ break;
584
+ }
585
+ this.updatePattern(patternId, { confidence });
586
+ }
587
+ // ─── Groups ──────────────────────────────────────────────────────
588
+ createGroup(input) {
589
+ this.initializeSync();
590
+ const id = `GRP-${uuidv4().substring(0, 8)}`;
591
+ const now = (/* @__PURE__ */ new Date()).toISOString();
592
+ this.db.run(
593
+ `INSERT INTO groups (
594
+ id, name, common_symbols, common_error_patterns,
595
+ suggested_pattern_id, first_seen, last_seen, created_at, updated_at
596
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
597
+ [
598
+ id,
599
+ input.name || null,
600
+ JSON.stringify(input.commonSymbols),
601
+ JSON.stringify(input.commonErrorPatterns),
602
+ input.suggestedPattern?.id || null,
603
+ input.firstSeen,
604
+ input.lastSeen,
605
+ now,
606
+ now
607
+ ]
608
+ );
609
+ for (const incidentId of input.incidents) {
610
+ this.addToGroup(id, incidentId);
611
+ }
612
+ this.save();
613
+ return id;
614
+ }
615
+ getGroup(id) {
616
+ this.initializeSync();
617
+ const result = this.db.exec("SELECT * FROM groups WHERE id = ?", [id]);
618
+ if (result.length === 0 || result[0].values.length === 0) return null;
619
+ return this.rowToGroup(result[0].columns, result[0].values[0]);
620
+ }
621
+ getGroups(options = {}) {
622
+ this.initializeSync();
623
+ const limit = options.limit || 100;
624
+ const result = this.db.exec(
625
+ "SELECT * FROM groups ORDER BY last_seen DESC LIMIT ?",
626
+ [limit]
627
+ );
628
+ if (result.length === 0) return [];
629
+ return result[0].values.map(
630
+ (row) => this.rowToGroup(result[0].columns, row)
631
+ );
632
+ }
633
+ addToGroup(groupId, incidentId) {
634
+ this.initializeSync();
635
+ const now = (/* @__PURE__ */ new Date()).toISOString();
636
+ this.db.run(
637
+ "INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
638
+ [groupId, incidentId, now]
639
+ );
640
+ this.db.run("UPDATE incidents SET group_id = ? WHERE id = ?", [
641
+ groupId,
642
+ incidentId
643
+ ]);
644
+ this.db.run(
645
+ "UPDATE groups SET last_seen = ?, updated_at = ? WHERE id = ?",
646
+ [now, now, groupId]
647
+ );
648
+ this.save();
649
+ }
650
+ // ─── Resolutions ─────────────────────────────────────────────────
651
+ recordResolution(resolution) {
652
+ this.initializeSync();
653
+ const id = uuidv4();
654
+ const now = (/* @__PURE__ */ new Date()).toISOString();
655
+ this.db.run(
656
+ `INSERT INTO resolutions (id, incident_id, pattern_id, commit_hash, pr_url, notes, resolved_at)
657
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
658
+ [
659
+ id,
660
+ resolution.incidentId,
661
+ resolution.patternId || null,
662
+ resolution.commitHash || null,
663
+ resolution.prUrl || null,
664
+ resolution.notes || null,
665
+ now
666
+ ]
667
+ );
668
+ this.updateIncident(resolution.incidentId, {
669
+ status: "resolved",
670
+ resolvedAt: now,
671
+ resolvedBy: resolution.patternId || "manual",
672
+ resolution: {
673
+ patternId: resolution.patternId,
674
+ commitHash: resolution.commitHash,
675
+ prUrl: resolution.prUrl,
676
+ notes: resolution.notes
677
+ }
678
+ });
679
+ if (resolution.patternId) {
680
+ this.updatePatternConfidence(resolution.patternId, "resolved");
681
+ }
682
+ this.save();
683
+ }
684
+ markRecurred(incidentId) {
685
+ this.initializeSync();
686
+ this.db.run(
687
+ "UPDATE resolutions SET recurred = 1 WHERE incident_id = ?",
688
+ [incidentId]
689
+ );
690
+ const result = this.db.exec(
691
+ "SELECT pattern_id FROM resolutions WHERE incident_id = ?",
692
+ [incidentId]
693
+ );
694
+ if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
695
+ this.updatePatternConfidence(result[0].values[0][0], "recurred");
696
+ }
697
+ this.save();
698
+ }
699
+ getResolutionHistory(options = {}) {
700
+ this.initializeSync();
701
+ const conditions = [];
702
+ const params = [];
703
+ if (options.patternId) {
704
+ conditions.push("pattern_id = ?");
705
+ params.push(options.patternId);
706
+ }
707
+ if (options.symbol) {
708
+ conditions.push(`incident_id IN (
709
+ SELECT id FROM incidents WHERE symbols LIKE ?
710
+ )`);
711
+ params.push(`%${options.symbol}%`);
712
+ }
713
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
714
+ const limit = options.limit || 100;
715
+ const result = this.db.exec(
716
+ `SELECT * FROM resolutions ${whereClause} ORDER BY resolved_at DESC LIMIT ?`,
717
+ [...params, limit]
718
+ );
719
+ if (result.length === 0) return [];
720
+ const columns = result[0].columns;
721
+ return result[0].values.map((row) => {
722
+ const obj = {};
723
+ columns.forEach((col, i) => {
724
+ obj[col] = row[i];
725
+ });
726
+ return {
727
+ id: obj.id,
728
+ incidentId: obj.incident_id,
729
+ patternId: obj.pattern_id || void 0,
730
+ commitHash: obj.commit_hash || void 0,
731
+ prUrl: obj.pr_url || void 0,
732
+ notes: obj.notes || void 0,
733
+ resolvedAt: obj.resolved_at,
734
+ recurred: obj.recurred === 1
735
+ };
736
+ });
737
+ }
738
+ // ─── Stats ───────────────────────────────────────────────────────
739
+ getStats(period) {
740
+ this.initializeSync();
741
+ const { start, end } = period;
742
+ const total = this.getIncidentCount({ dateFrom: start, dateTo: end });
743
+ const open = this.getIncidentCount({
744
+ dateFrom: start,
745
+ dateTo: end,
746
+ status: "open"
747
+ });
748
+ const resolved = this.getIncidentCount({
749
+ dateFrom: start,
750
+ dateTo: end,
751
+ status: "resolved"
752
+ });
753
+ const envResult = this.db.exec(
754
+ `SELECT environment, COUNT(*) as count
755
+ FROM incidents
756
+ WHERE timestamp >= ? AND timestamp <= ?
757
+ GROUP BY environment`,
758
+ [start, end]
759
+ );
760
+ const byEnvironment = {};
761
+ if (envResult.length > 0) {
762
+ for (const row of envResult[0].values) {
763
+ byEnvironment[row[0]] = row[1];
764
+ }
765
+ }
766
+ const dayResult = this.db.exec(
767
+ `SELECT DATE(timestamp) as date, COUNT(*) as count
768
+ FROM incidents
769
+ WHERE timestamp >= ? AND timestamp <= ?
770
+ GROUP BY DATE(timestamp)
771
+ ORDER BY date`,
772
+ [start, end]
773
+ );
774
+ const byDay = [];
775
+ if (dayResult.length > 0) {
776
+ for (const row of dayResult[0].values) {
777
+ byDay.push({ date: row[0], count: row[1] });
778
+ }
779
+ }
780
+ const patterns = this.getAllPatterns({ includePrivate: true });
781
+ const avgConfidence = patterns.length > 0 ? patterns.reduce((sum, p) => sum + p.confidence.score, 0) / patterns.length : 0;
782
+ const mostEffective = patterns.sort((a, b) => b.confidence.timesResolved - a.confidence.timesResolved).slice(0, 5).map((p) => ({ patternId: p.id, resolvedCount: p.confidence.timesResolved }));
783
+ const leastEffective = patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
784
+ patternId: p.id,
785
+ recurrenceRate: p.confidence.timesRecurred / Math.max(1, p.confidence.timesResolved)
786
+ })).sort((a, b) => b.recurrenceRate - a.recurrenceRate).slice(0, 5);
787
+ const symbolCounts = /* @__PURE__ */ new Map();
788
+ const incidents = this.getRecentIncidents({
789
+ dateFrom: start,
790
+ dateTo: end,
791
+ limit: 1e3
792
+ });
793
+ for (const incident of incidents) {
794
+ for (const [, value] of Object.entries(incident.symbols)) {
795
+ if (value) {
796
+ symbolCounts.set(value, (symbolCounts.get(value) || 0) + 1);
797
+ }
798
+ }
799
+ }
800
+ const mostIncidents = Array.from(symbolCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([symbol, count]) => ({ symbol, count }));
801
+ const resolutions = this.getResolutionHistory({ limit: 1e3 });
802
+ const periodResolutions = resolutions.filter(
803
+ (r) => r.resolvedAt >= start && r.resolvedAt <= end
804
+ );
805
+ const resolvedWithPattern = periodResolutions.filter(
806
+ (r) => r.patternId
807
+ ).length;
808
+ const resolvedManually = periodResolutions.length - resolvedWithPattern;
809
+ return {
810
+ period: { start, end },
811
+ incidents: {
812
+ total,
813
+ open,
814
+ resolved,
815
+ byEnvironment,
816
+ byDay
817
+ },
818
+ patterns: {
819
+ total: patterns.length,
820
+ avgConfidence: Math.round(avgConfidence),
821
+ mostEffective,
822
+ leastEffective
823
+ },
824
+ symbols: {
825
+ mostIncidents,
826
+ mostResolved: [],
827
+ hotspots: mostIncidents.slice(0, 5).map((s) => ({
828
+ symbol: s.symbol,
829
+ incidentRate: s.count / Math.max(1, total)
830
+ }))
831
+ },
832
+ resolution: {
833
+ avgTimeToResolve: 0,
834
+ resolvedWithPattern,
835
+ resolvedManually,
836
+ resolutionRate: total > 0 ? resolved / total * 100 : 0
837
+ }
838
+ };
839
+ }
840
+ getSymbolHealth(symbol) {
841
+ const incidents = this.getRecentIncidents({ symbol, limit: 1e3 });
842
+ const incidentCount = incidents.length;
843
+ const patternCounts = /* @__PURE__ */ new Map();
844
+ for (const incident of incidents) {
845
+ if (incident.resolution?.patternId) {
846
+ const count = patternCounts.get(incident.resolution.patternId) || 0;
847
+ patternCounts.set(incident.resolution.patternId, count + 1);
848
+ }
849
+ }
850
+ const topPatterns = Array.from(patternCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([patternId, count]) => ({ patternId, count }));
851
+ return {
852
+ incidentCount,
853
+ avgTimeToResolve: 0,
854
+ topPatterns
855
+ };
856
+ }
857
+ // ─── Import/Export ───────────────────────────────────────────────
858
+ exportPatterns(options = {}) {
859
+ const patterns = this.getAllPatterns({
860
+ includePrivate: options.includePrivate
861
+ });
862
+ return {
863
+ version: "1.0.0",
864
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
865
+ patterns
866
+ };
867
+ }
868
+ importPatterns(data, options = {}) {
869
+ let imported = 0;
870
+ let skipped = 0;
871
+ for (const pattern of data.patterns) {
872
+ const existing = this.getPattern(pattern.id);
873
+ if (existing && !options.overwrite) {
874
+ skipped++;
875
+ continue;
876
+ }
877
+ if (existing) {
878
+ this.updatePattern(pattern.id, pattern);
879
+ } else {
880
+ this.addPattern({
881
+ ...pattern,
882
+ source: "imported"
883
+ });
884
+ }
885
+ imported++;
886
+ }
887
+ return { imported, skipped };
888
+ }
889
+ exportBackup() {
890
+ const incidents = this.getRecentIncidents({ limit: 1e5 });
891
+ const patterns = this.getAllPatterns({ includePrivate: true });
892
+ const groups = this.getGroups({ limit: 1e4 });
893
+ return {
894
+ version: "1.0.0",
895
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
896
+ incidents,
897
+ patterns,
898
+ groups
899
+ };
900
+ }
901
+ importBackup(data) {
902
+ this.initializeSync();
903
+ this.db.run("DELETE FROM group_members");
904
+ this.db.run("DELETE FROM resolutions");
905
+ this.db.run("DELETE FROM groups");
906
+ this.db.run("DELETE FROM incidents");
907
+ this.db.run("DELETE FROM patterns");
908
+ for (const pattern of data.patterns) {
909
+ this.addPattern(pattern);
910
+ }
911
+ for (const incident of data.incidents) {
912
+ const now2 = incident.timestamp;
913
+ this.db.run(
914
+ `INSERT INTO incidents (
915
+ id, timestamp, status, error_message, error_stack, error_code, error_type,
916
+ symbols, flow_position, environment, service, version, user_id, request_id,
917
+ group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
918
+ created_at, updated_at
919
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
920
+ [
921
+ incident.id,
922
+ incident.timestamp,
923
+ incident.status,
924
+ incident.error.message,
925
+ incident.error.stack || null,
926
+ incident.error.code || null,
927
+ incident.error.type || null,
928
+ JSON.stringify(incident.symbols),
929
+ incident.flowPosition ? JSON.stringify(incident.flowPosition) : null,
930
+ incident.environment,
931
+ incident.service || null,
932
+ incident.version || null,
933
+ incident.userId || null,
934
+ incident.requestId || null,
935
+ incident.groupId || null,
936
+ JSON.stringify(incident.notes),
937
+ JSON.stringify(incident.relatedIncidents),
938
+ incident.resolvedAt || null,
939
+ incident.resolvedBy || null,
940
+ incident.resolution ? JSON.stringify(incident.resolution) : null,
941
+ now2,
942
+ now2
943
+ ]
944
+ );
945
+ }
946
+ const result = this.db.exec(
947
+ "SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
948
+ );
949
+ if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
950
+ this.incidentCounter = result[0].values[0][0];
951
+ }
952
+ const now = (/* @__PURE__ */ new Date()).toISOString();
953
+ for (const group of data.groups) {
954
+ this.db.run(
955
+ `INSERT INTO groups (
956
+ id, name, common_symbols, common_error_patterns,
957
+ suggested_pattern_id, first_seen, last_seen, created_at, updated_at
958
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
959
+ [
960
+ group.id,
961
+ group.name || null,
962
+ JSON.stringify(group.commonSymbols),
963
+ JSON.stringify(group.commonErrorPatterns),
964
+ group.suggestedPattern?.id || null,
965
+ group.firstSeen,
966
+ group.lastSeen,
967
+ now,
968
+ now
969
+ ]
970
+ );
971
+ for (const incidentId of group.incidents) {
972
+ this.db.run(
973
+ "INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
974
+ [group.id, incidentId, now]
975
+ );
976
+ }
977
+ }
978
+ this.save();
979
+ }
980
+ // ─── Helper Methods ──────────────────────────────────────────────
981
+ rowToIncident(columns, row) {
982
+ const obj = {};
983
+ columns.forEach((col, i) => {
984
+ obj[col] = row[i];
985
+ });
986
+ return {
987
+ id: obj.id,
988
+ timestamp: obj.timestamp,
989
+ status: obj.status,
990
+ error: {
991
+ message: obj.error_message,
992
+ stack: obj.error_stack || void 0,
993
+ code: obj.error_code || void 0,
994
+ type: obj.error_type || void 0
995
+ },
996
+ symbols: JSON.parse(obj.symbols || "{}"),
997
+ flowPosition: obj.flow_position ? JSON.parse(obj.flow_position) : void 0,
998
+ environment: obj.environment,
999
+ service: obj.service || void 0,
1000
+ version: obj.version || void 0,
1001
+ userId: obj.user_id || void 0,
1002
+ requestId: obj.request_id || void 0,
1003
+ groupId: obj.group_id || void 0,
1004
+ notes: JSON.parse(obj.notes || "[]"),
1005
+ relatedIncidents: JSON.parse(obj.related_incidents || "[]"),
1006
+ resolvedAt: obj.resolved_at || void 0,
1007
+ resolvedBy: obj.resolved_by || void 0,
1008
+ resolution: obj.resolution ? JSON.parse(obj.resolution) : void 0
1009
+ };
1010
+ }
1011
+ rowToPattern(columns, row) {
1012
+ const obj = {};
1013
+ columns.forEach((col, i) => {
1014
+ obj[col] = row[i];
1015
+ });
1016
+ return {
1017
+ id: obj.id,
1018
+ name: obj.name,
1019
+ description: obj.description || "",
1020
+ pattern: JSON.parse(obj.pattern),
1021
+ resolution: JSON.parse(obj.resolution),
1022
+ confidence: JSON.parse(obj.confidence),
1023
+ source: obj.source,
1024
+ private: obj.private === 1,
1025
+ tags: JSON.parse(obj.tags || "[]"),
1026
+ createdAt: obj.created_at,
1027
+ updatedAt: obj.updated_at
1028
+ };
1029
+ }
1030
+ rowToGroup(columns, row) {
1031
+ const obj = {};
1032
+ columns.forEach((col, i) => {
1033
+ obj[col] = row[i];
1034
+ });
1035
+ const membersResult = this.db.exec(
1036
+ "SELECT incident_id FROM group_members WHERE group_id = ?",
1037
+ [obj.id]
1038
+ );
1039
+ const incidents = [];
1040
+ if (membersResult.length > 0) {
1041
+ for (const r of membersResult[0].values) {
1042
+ incidents.push(r[0]);
1043
+ }
1044
+ }
1045
+ const envResult = this.db.exec(
1046
+ `SELECT DISTINCT environment FROM incidents
1047
+ WHERE id IN (SELECT incident_id FROM group_members WHERE group_id = ?)`,
1048
+ [obj.id]
1049
+ );
1050
+ const environments = [];
1051
+ if (envResult.length > 0) {
1052
+ for (const r of envResult[0].values) {
1053
+ environments.push(r[0]);
1054
+ }
1055
+ }
1056
+ return {
1057
+ id: obj.id,
1058
+ name: obj.name || void 0,
1059
+ incidents,
1060
+ commonSymbols: JSON.parse(obj.common_symbols || "{}"),
1061
+ commonErrorPatterns: JSON.parse(
1062
+ obj.common_error_patterns || "[]"
1063
+ ),
1064
+ count: incidents.length,
1065
+ firstSeen: obj.first_seen,
1066
+ lastSeen: obj.last_seen,
1067
+ environments,
1068
+ suggestedPattern: obj.suggested_pattern_id ? this.getPattern(obj.suggested_pattern_id) || void 0 : void 0
1069
+ };
1070
+ }
1071
+ // ─── Practice Events ─────────────────────────────────────────────
1072
+ recordPracticeEvent(input) {
1073
+ this.initializeSync();
1074
+ const id = `PE-${uuidv4().substring(0, 8)}`;
1075
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1076
+ this.db.run(
1077
+ `INSERT INTO practice_events (
1078
+ id, timestamp, habit_id, habit_category, result,
1079
+ engineer, session_id, lore_entry_id, task_description,
1080
+ symbols_touched, files_modified, related_incident_id, notes
1081
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1082
+ [
1083
+ id,
1084
+ now,
1085
+ input.habitId,
1086
+ input.habitCategory,
1087
+ input.result,
1088
+ input.engineer,
1089
+ input.sessionId,
1090
+ input.loreEntryId || null,
1091
+ input.taskDescription || null,
1092
+ JSON.stringify(input.symbolsTouched || []),
1093
+ JSON.stringify(input.filesModified || []),
1094
+ input.relatedIncidentId || null,
1095
+ input.notes || null
1096
+ ]
1097
+ );
1098
+ this.save();
1099
+ return id;
1100
+ }
1101
+ getPracticeEvents(options = {}) {
1102
+ this.initializeSync();
1103
+ const { limit = 100, offset = 0 } = options;
1104
+ const conditions = [];
1105
+ const params = [];
1106
+ if (options.habitId) {
1107
+ conditions.push("habit_id = ?");
1108
+ params.push(options.habitId);
1109
+ }
1110
+ if (options.habitCategory) {
1111
+ conditions.push("habit_category = ?");
1112
+ params.push(options.habitCategory);
1113
+ }
1114
+ if (options.result) {
1115
+ conditions.push("result = ?");
1116
+ params.push(options.result);
1117
+ }
1118
+ if (options.engineer) {
1119
+ conditions.push("engineer = ?");
1120
+ params.push(options.engineer);
1121
+ }
1122
+ if (options.sessionId) {
1123
+ conditions.push("session_id = ?");
1124
+ params.push(options.sessionId);
1125
+ }
1126
+ if (options.dateFrom) {
1127
+ conditions.push("timestamp >= ?");
1128
+ params.push(options.dateFrom);
1129
+ }
1130
+ if (options.dateTo) {
1131
+ conditions.push("timestamp <= ?");
1132
+ params.push(options.dateTo);
1133
+ }
1134
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1135
+ const result = this.db.exec(
1136
+ `SELECT * FROM practice_events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1137
+ [...params, limit, offset]
1138
+ );
1139
+ if (result.length === 0) return [];
1140
+ return result[0].values.map(
1141
+ (row) => this.rowToPracticeEvent(result[0].columns, row)
1142
+ );
1143
+ }
1144
+ getPracticeEventCount(options = {}) {
1145
+ this.initializeSync();
1146
+ const conditions = [];
1147
+ const params = [];
1148
+ if (options.habitId) {
1149
+ conditions.push("habit_id = ?");
1150
+ params.push(options.habitId);
1151
+ }
1152
+ if (options.habitCategory) {
1153
+ conditions.push("habit_category = ?");
1154
+ params.push(options.habitCategory);
1155
+ }
1156
+ if (options.result) {
1157
+ conditions.push("result = ?");
1158
+ params.push(options.result);
1159
+ }
1160
+ if (options.engineer) {
1161
+ conditions.push("engineer = ?");
1162
+ params.push(options.engineer);
1163
+ }
1164
+ if (options.dateFrom) {
1165
+ conditions.push("timestamp >= ?");
1166
+ params.push(options.dateFrom);
1167
+ }
1168
+ if (options.dateTo) {
1169
+ conditions.push("timestamp <= ?");
1170
+ params.push(options.dateTo);
1171
+ }
1172
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1173
+ const result = this.db.exec(
1174
+ `SELECT COUNT(*) as count FROM practice_events ${whereClause}`,
1175
+ params
1176
+ );
1177
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1178
+ return result[0].values[0][0];
1179
+ }
1180
+ getComplianceRate(options = {}) {
1181
+ this.initializeSync();
1182
+ const conditions = [];
1183
+ const params = [];
1184
+ if (options.habitId) {
1185
+ conditions.push("habit_id = ?");
1186
+ params.push(options.habitId);
1187
+ }
1188
+ if (options.habitCategory) {
1189
+ conditions.push("habit_category = ?");
1190
+ params.push(options.habitCategory);
1191
+ }
1192
+ if (options.engineer) {
1193
+ conditions.push("engineer = ?");
1194
+ params.push(options.engineer);
1195
+ }
1196
+ if (options.dateFrom) {
1197
+ conditions.push("timestamp >= ?");
1198
+ params.push(options.dateFrom);
1199
+ }
1200
+ if (options.dateTo) {
1201
+ conditions.push("timestamp <= ?");
1202
+ params.push(options.dateTo);
1203
+ }
1204
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1205
+ const result = this.db.exec(
1206
+ `SELECT result, COUNT(*) as count
1207
+ FROM practice_events ${whereClause}
1208
+ GROUP BY result`,
1209
+ params
1210
+ );
1211
+ let followed = 0;
1212
+ let skipped = 0;
1213
+ let partial = 0;
1214
+ if (result.length > 0) {
1215
+ for (const row of result[0].values) {
1216
+ const r = row[0];
1217
+ const count = row[1];
1218
+ if (r === "followed") followed = count;
1219
+ else if (r === "skipped") skipped = count;
1220
+ else if (r === "partial") partial = count;
1221
+ }
1222
+ }
1223
+ const total = followed + skipped + partial;
1224
+ const rate = total > 0 ? (followed + partial * 0.5) / total * 100 : 100;
1225
+ return { total, followed, skipped, partial, rate: Math.round(rate) };
1226
+ }
1227
+ rowToPracticeEvent(columns, row) {
1228
+ const obj = {};
1229
+ columns.forEach((col, i) => {
1230
+ obj[col] = row[i];
1231
+ });
1232
+ return {
1233
+ id: obj.id,
1234
+ timestamp: obj.timestamp,
1235
+ habitId: obj.habit_id,
1236
+ habitCategory: obj.habit_category,
1237
+ result: obj.result,
1238
+ engineer: obj.engineer,
1239
+ sessionId: obj.session_id,
1240
+ loreEntryId: obj.lore_entry_id || void 0,
1241
+ taskDescription: obj.task_description || void 0,
1242
+ symbolsTouched: JSON.parse(obj.symbols_touched || "[]"),
1243
+ filesModified: JSON.parse(obj.files_modified || "[]"),
1244
+ relatedIncidentId: obj.related_incident_id || void 0,
1245
+ notes: obj.notes || void 0
1246
+ };
1247
+ }
1248
+ close() {
1249
+ if (this.db) {
1250
+ this.save();
1251
+ this.db.close();
1252
+ this.db = null;
1253
+ }
1254
+ }
1255
+ };
1256
+
1257
+ // src/matcher.ts
1258
+ var DEFAULT_CONFIG = {
1259
+ minScore: 30,
1260
+ maxResults: 5,
1261
+ boostConfidence: true
1262
+ };
1263
+ var PatternMatcher = class {
1264
+ constructor(storage2) {
1265
+ this.storage = storage2;
1266
+ }
1267
+ /**
1268
+ * Match an incident against all patterns and return ranked results
1269
+ */
1270
+ match(incident, config = {}) {
1271
+ const { minScore, maxResults, boostConfidence } = {
1272
+ ...DEFAULT_CONFIG,
1273
+ ...config
1274
+ };
1275
+ const patterns = this.storage.getAllPatterns({ includePrivate: true });
1276
+ const matches = [];
1277
+ for (const pattern of patterns) {
1278
+ if (!this.matchEnvironment(pattern, incident)) {
1279
+ continue;
1280
+ }
1281
+ const { score, matchedCriteria } = this.scoreMatch(pattern, incident);
1282
+ if (score >= minScore) {
1283
+ let confidence = score;
1284
+ if (boostConfidence) {
1285
+ const confidenceFactor = pattern.confidence.score / 100;
1286
+ confidence = score * (0.5 + 0.5 * confidenceFactor);
1287
+ }
1288
+ matches.push({
1289
+ pattern,
1290
+ score,
1291
+ matchedCriteria,
1292
+ confidence: Math.round(confidence)
1293
+ });
1294
+ this.storage.updatePatternConfidence(pattern.id, "matched");
1295
+ }
1296
+ }
1297
+ return matches.sort((a, b) => b.confidence - a.confidence).slice(0, maxResults);
1298
+ }
1299
+ /**
1300
+ * Test a pattern against historical incidents
1301
+ */
1302
+ testPattern(pattern, limit = 100) {
1303
+ const incidents = this.storage.getRecentIncidents({ limit });
1304
+ const wouldMatch = [];
1305
+ let totalScore = 0;
1306
+ for (const incident of incidents) {
1307
+ if (!this.matchEnvironment(pattern, incident)) {
1308
+ continue;
1309
+ }
1310
+ const { score } = this.scoreMatch(pattern, incident);
1311
+ if (score >= 30) {
1312
+ wouldMatch.push(incident);
1313
+ totalScore += score;
1314
+ }
1315
+ }
1316
+ return {
1317
+ wouldMatch,
1318
+ matchCount: wouldMatch.length,
1319
+ avgScore: wouldMatch.length > 0 ? Math.round(totalScore / wouldMatch.length) : 0
1320
+ };
1321
+ }
1322
+ /**
1323
+ * Score how well a pattern matches an incident
1324
+ */
1325
+ scoreMatch(pattern, incident) {
1326
+ let score = 0;
1327
+ const matchedCriteria = {
1328
+ symbols: [],
1329
+ errorKeywords: [],
1330
+ missingSignals: []
1331
+ };
1332
+ const symbolScore = this.matchSymbols(
1333
+ pattern.pattern.symbols,
1334
+ incident.symbols,
1335
+ matchedCriteria.symbols
1336
+ );
1337
+ score += Math.min(symbolScore, 50);
1338
+ const errorScore = this.matchErrorText(
1339
+ pattern,
1340
+ incident,
1341
+ matchedCriteria.errorKeywords
1342
+ );
1343
+ score += Math.min(errorScore, 25);
1344
+ const signalScore = this.matchMissingSignals(
1345
+ pattern,
1346
+ incident,
1347
+ matchedCriteria.missingSignals
1348
+ );
1349
+ score += Math.min(signalScore, 25);
1350
+ score = Math.min(score, 100);
1351
+ return { score, matchedCriteria };
1352
+ }
1353
+ /**
1354
+ * Match symbols between pattern and incident
1355
+ */
1356
+ matchSymbols(patternSymbols, incidentSymbols, matched) {
1357
+ let score = 0;
1358
+ const symbolTypes = [
1359
+ "feature",
1360
+ "component",
1361
+ "flow",
1362
+ "gate",
1363
+ "signal",
1364
+ "state",
1365
+ "integration"
1366
+ ];
1367
+ for (const type of symbolTypes) {
1368
+ const patternValue = patternSymbols[type];
1369
+ const incidentValue = incidentSymbols[type];
1370
+ if (!patternValue || !incidentValue) {
1371
+ continue;
1372
+ }
1373
+ if (typeof patternValue === "string") {
1374
+ if (this.matchSingleSymbol(patternValue, incidentValue)) {
1375
+ score += patternValue.includes("*") ? 5 : 10;
1376
+ matched.push(type);
1377
+ }
1378
+ } else if (Array.isArray(patternValue)) {
1379
+ for (const pv of patternValue) {
1380
+ if (this.matchSingleSymbol(pv, incidentValue)) {
1381
+ score += 7;
1382
+ matched.push(type);
1383
+ break;
1384
+ }
1385
+ }
1386
+ }
1387
+ }
1388
+ return score;
1389
+ }
1390
+ /**
1391
+ * Match a single symbol value (supports wildcards)
1392
+ */
1393
+ matchSingleSymbol(pattern, value) {
1394
+ if (pattern === "*") {
1395
+ return true;
1396
+ }
1397
+ if (pattern.endsWith("*")) {
1398
+ const prefix = pattern.slice(0, -1);
1399
+ return value.startsWith(prefix);
1400
+ }
1401
+ if (pattern.startsWith("*")) {
1402
+ const suffix = pattern.slice(1);
1403
+ return value.endsWith(suffix);
1404
+ }
1405
+ if (pattern.includes("*")) {
1406
+ const regex = new RegExp(
1407
+ "^" + pattern.replace(/\*/g, ".*") + "$"
1408
+ );
1409
+ return regex.test(value);
1410
+ }
1411
+ return pattern === value;
1412
+ }
1413
+ /**
1414
+ * Match error text keywords and regex
1415
+ */
1416
+ matchErrorText(pattern, incident, matched) {
1417
+ let score = 0;
1418
+ const errorMessage = incident.error.message.toLowerCase();
1419
+ const errorType = incident.error.type?.toLowerCase();
1420
+ if (pattern.pattern.errorContains) {
1421
+ for (const keyword of pattern.pattern.errorContains) {
1422
+ if (errorMessage.includes(keyword.toLowerCase())) {
1423
+ score += 5;
1424
+ matched.push(keyword);
1425
+ }
1426
+ }
1427
+ }
1428
+ if (pattern.pattern.errorMatches) {
1429
+ try {
1430
+ const regex = new RegExp(pattern.pattern.errorMatches, "i");
1431
+ if (regex.test(incident.error.message)) {
1432
+ score += 10;
1433
+ matched.push(`regex:${pattern.pattern.errorMatches}`);
1434
+ }
1435
+ } catch {
1436
+ }
1437
+ }
1438
+ if (pattern.pattern.errorType && errorType) {
1439
+ for (const type of pattern.pattern.errorType) {
1440
+ if (errorType.includes(type.toLowerCase())) {
1441
+ score += 5;
1442
+ matched.push(`type:${type}`);
1443
+ }
1444
+ }
1445
+ }
1446
+ return score;
1447
+ }
1448
+ /**
1449
+ * Match missing signals from flow position
1450
+ */
1451
+ matchMissingSignals(pattern, incident, matched) {
1452
+ if (!pattern.pattern.missingSignals || !incident.flowPosition?.missing) {
1453
+ return 0;
1454
+ }
1455
+ let score = 0;
1456
+ for (const expectedSignal of pattern.pattern.missingSignals) {
1457
+ for (const missingSignal of incident.flowPosition.missing) {
1458
+ if (this.matchSingleSymbol(expectedSignal, missingSignal)) {
1459
+ score += 12;
1460
+ matched.push(missingSignal);
1461
+ break;
1462
+ }
1463
+ }
1464
+ }
1465
+ return score;
1466
+ }
1467
+ /**
1468
+ * Check if pattern's environment filter matches incident
1469
+ */
1470
+ matchEnvironment(pattern, incident) {
1471
+ if (!pattern.pattern.environment || pattern.pattern.environment.length === 0) {
1472
+ return true;
1473
+ }
1474
+ return pattern.pattern.environment.includes(incident.environment);
1475
+ }
1476
+ };
1477
+
1478
+ // src/timeline.ts
1479
+ var TimelineBuilder = class {
1480
+ /**
1481
+ * Build a timeline from an incident with flow position
1482
+ */
1483
+ build(incident) {
1484
+ if (!incident.flowPosition) {
1485
+ return null;
1486
+ }
1487
+ const events = [];
1488
+ const baseTime = new Date(incident.timestamp).getTime();
1489
+ events.push({
1490
+ timestamp: new Date(baseTime - 5e3).toISOString(),
1491
+ symbol: incident.flowPosition.flowId,
1492
+ type: "flow-started"
1493
+ });
1494
+ let eventOffset = 1e3;
1495
+ for (const signal of incident.flowPosition.actual) {
1496
+ const type = this.inferEventType(signal);
1497
+ events.push({
1498
+ timestamp: new Date(baseTime - 4e3 + eventOffset).toISOString(),
1499
+ symbol: signal,
1500
+ type
1501
+ });
1502
+ eventOffset += Math.random() * 1e3 + 500;
1503
+ }
1504
+ const failedSymbol = incident.flowPosition.failedAt || incident.flowPosition.missing[0] || incident.symbols.gate || incident.symbols.signal || "unknown";
1505
+ events.push({
1506
+ timestamp: incident.timestamp,
1507
+ symbol: failedSymbol,
1508
+ type: "error",
1509
+ data: {
1510
+ message: incident.error.message,
1511
+ missing: incident.flowPosition.missing
1512
+ }
1513
+ });
1514
+ return {
1515
+ incidentId: incident.id,
1516
+ flowId: incident.flowPosition.flowId,
1517
+ events,
1518
+ failure: {
1519
+ at: incident.timestamp,
1520
+ symbol: failedSymbol,
1521
+ reason: incident.error.message
1522
+ }
1523
+ };
1524
+ }
1525
+ /**
1526
+ * Render timeline as ASCII art
1527
+ */
1528
+ renderAscii(timeline) {
1529
+ const lines = [];
1530
+ lines.push(`${timeline.flowId} Timeline`);
1531
+ lines.push("\u2550".repeat(40));
1532
+ lines.push("");
1533
+ for (const event of timeline.events) {
1534
+ const time = this.formatTime(event.timestamp);
1535
+ const icon = this.getEventIcon(event.type);
1536
+ const status = this.getEventStatus(event.type);
1537
+ let line = `${time} ${icon} ${event.symbol}`;
1538
+ if (status) {
1539
+ line += ` (${status})`;
1540
+ }
1541
+ lines.push(line);
1542
+ if (event.type === "error" && event.data) {
1543
+ lines.push(` \u2514\u2500 ${event.data.message}`);
1544
+ if (event.data.missing && Array.isArray(event.data.missing) && event.data.missing.length > 0) {
1545
+ lines.push(
1546
+ ` \u2514\u2500 Expected: ${event.data.missing.join(", ")}`
1547
+ );
1548
+ }
1549
+ }
1550
+ }
1551
+ const missing = timeline.events.find((e) => e.type === "error")?.data?.missing;
1552
+ if (missing && missing.length > 0) {
1553
+ lines.push("");
1554
+ lines.push(`Missing signals: ${missing.join(", ")}`);
1555
+ }
1556
+ return lines.join("\n");
1557
+ }
1558
+ /**
1559
+ * Render timeline as structured data (for MCP/JSON output)
1560
+ */
1561
+ renderStructured(timeline) {
1562
+ return {
1563
+ incidentId: timeline.incidentId,
1564
+ flow: {
1565
+ id: timeline.flowId,
1566
+ eventCount: timeline.events.length
1567
+ },
1568
+ events: timeline.events.map((event) => ({
1569
+ time: this.formatTime(event.timestamp),
1570
+ symbol: event.symbol,
1571
+ type: event.type,
1572
+ status: this.getEventStatus(event.type),
1573
+ data: event.data
1574
+ })),
1575
+ failure: {
1576
+ at: this.formatTime(timeline.failure.at),
1577
+ symbol: timeline.failure.symbol,
1578
+ reason: timeline.failure.reason
1579
+ }
1580
+ };
1581
+ }
1582
+ /**
1583
+ * Infer event type from symbol prefix
1584
+ */
1585
+ inferEventType(symbol) {
1586
+ if (symbol.startsWith("^")) {
1587
+ return "gate-passed";
1588
+ }
1589
+ if (symbol.startsWith("!")) {
1590
+ return "signal-emitted";
1591
+ }
1592
+ if (symbol.startsWith("%")) {
1593
+ return "state-changed";
1594
+ }
1595
+ return "signal-emitted";
1596
+ }
1597
+ /**
1598
+ * Get icon for event type
1599
+ */
1600
+ getEventIcon(type) {
1601
+ switch (type) {
1602
+ case "flow-started":
1603
+ return "\u25B6";
1604
+ case "flow-ended":
1605
+ return "\u25A0";
1606
+ case "gate-passed":
1607
+ return "\u2713";
1608
+ case "gate-failed":
1609
+ return "\u2717";
1610
+ case "signal-emitted":
1611
+ return "\u26A1";
1612
+ case "state-changed":
1613
+ return "\u25C6";
1614
+ case "error":
1615
+ return "\u2717";
1616
+ default:
1617
+ return "\u2022";
1618
+ }
1619
+ }
1620
+ /**
1621
+ * Get status text for event type
1622
+ */
1623
+ getEventStatus(type) {
1624
+ switch (type) {
1625
+ case "gate-passed":
1626
+ return "PASSED";
1627
+ case "gate-failed":
1628
+ return "FAILED";
1629
+ case "signal-emitted":
1630
+ return "EMITTED";
1631
+ case "state-changed":
1632
+ return "CHANGED";
1633
+ case "error":
1634
+ return "ERROR";
1635
+ default:
1636
+ return "";
1637
+ }
1638
+ }
1639
+ /**
1640
+ * Format timestamp for display
1641
+ */
1642
+ formatTime(timestamp) {
1643
+ const date = new Date(timestamp);
1644
+ const hours = String(date.getHours()).padStart(2, "0");
1645
+ const minutes = String(date.getMinutes()).padStart(2, "0");
1646
+ const seconds = String(date.getSeconds()).padStart(2, "0");
1647
+ const millis = String(date.getMilliseconds()).padStart(3, "0");
1648
+ return `${hours}:${minutes}:${seconds}.${millis}`;
1649
+ }
1650
+ };
1651
+
1652
+ // src/stats.ts
1653
+ var StatsCalculator = class {
1654
+ constructor(storage2) {
1655
+ this.storage = storage2;
1656
+ }
1657
+ /**
1658
+ * Get comprehensive statistics for a time period
1659
+ */
1660
+ getStats(periodDays = 7) {
1661
+ const end = (/* @__PURE__ */ new Date()).toISOString();
1662
+ const start = new Date(
1663
+ Date.now() - periodDays * 24 * 60 * 60 * 1e3
1664
+ ).toISOString();
1665
+ return this.storage.getStats({ start, end });
1666
+ }
1667
+ /**
1668
+ * Get health metrics for a specific symbol
1669
+ */
1670
+ getSymbolHealth(symbol) {
1671
+ return this.storage.getSymbolHealth(symbol);
1672
+ }
1673
+ /**
1674
+ * Get trending issues (symbols with increasing incident rates)
1675
+ */
1676
+ getTrendingIssues(days = 7) {
1677
+ const now = Date.now();
1678
+ const halfPeriod = days * 24 * 60 * 60 * 1e3 / 2;
1679
+ const firstHalfStart = new Date(now - days * 24 * 60 * 60 * 1e3).toISOString();
1680
+ const midpoint = new Date(now - halfPeriod).toISOString();
1681
+ const secondHalfEnd = new Date(now).toISOString();
1682
+ const firstHalfIncidents = this.storage.getRecentIncidents({
1683
+ dateFrom: firstHalfStart,
1684
+ dateTo: midpoint,
1685
+ limit: 1e3
1686
+ });
1687
+ const secondHalfIncidents = this.storage.getRecentIncidents({
1688
+ dateFrom: midpoint,
1689
+ dateTo: secondHalfEnd,
1690
+ limit: 1e3
1691
+ });
1692
+ const firstHalfCounts = this.countSymbols(firstHalfIncidents);
1693
+ const secondHalfCounts = this.countSymbols(secondHalfIncidents);
1694
+ const trends = [];
1695
+ const allSymbols = /* @__PURE__ */ new Set([
1696
+ ...firstHalfCounts.keys(),
1697
+ ...secondHalfCounts.keys()
1698
+ ]);
1699
+ for (const symbol of allSymbols) {
1700
+ const first = firstHalfCounts.get(symbol) || 0;
1701
+ const second = secondHalfCounts.get(symbol) || 0;
1702
+ if (first === 0 && second > 0) {
1703
+ trends.push({ symbol, trend: second * 100 });
1704
+ } else if (first > 0) {
1705
+ const change = (second - first) / first * 100;
1706
+ trends.push({ symbol, trend: change });
1707
+ }
1708
+ }
1709
+ return trends.filter((t) => t.trend > 0).sort((a, b) => b.trend - a.trend).slice(0, 10);
1710
+ }
1711
+ /**
1712
+ * Get resolution metrics
1713
+ */
1714
+ getResolutionMetrics() {
1715
+ const stats = this.getStats(30);
1716
+ return {
1717
+ avgTimeToResolve: stats.resolution.avgTimeToResolve,
1718
+ resolvedWithPattern: stats.resolution.resolvedWithPattern,
1719
+ resolvedManually: stats.resolution.resolvedManually,
1720
+ totalResolved: stats.incidents.resolved,
1721
+ resolutionRate: stats.resolution.resolutionRate
1722
+ };
1723
+ }
1724
+ /**
1725
+ * Get pattern effectiveness metrics
1726
+ */
1727
+ getPatternEffectiveness() {
1728
+ const patterns = this.storage.getAllPatterns({ includePrivate: true });
1729
+ return patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
1730
+ patternId: p.id,
1731
+ name: p.name,
1732
+ matches: p.confidence.timesMatched,
1733
+ resolutions: p.confidence.timesResolved,
1734
+ recurrences: p.confidence.timesRecurred,
1735
+ effectiveness: p.confidence.timesMatched > 0 ? Math.round(
1736
+ (p.confidence.timesResolved - p.confidence.timesRecurred) / p.confidence.timesMatched * 100
1737
+ ) : 0
1738
+ })).sort((a, b) => b.effectiveness - a.effectiveness);
1739
+ }
1740
+ /**
1741
+ * Get incident rate by hour of day
1742
+ */
1743
+ getIncidentsByHour(days = 7) {
1744
+ const start = new Date(
1745
+ Date.now() - days * 24 * 60 * 60 * 1e3
1746
+ ).toISOString();
1747
+ const incidents = this.storage.getRecentIncidents({
1748
+ dateFrom: start,
1749
+ limit: 1e4
1750
+ });
1751
+ const hourCounts = /* @__PURE__ */ new Map();
1752
+ for (let i = 0; i < 24; i++) {
1753
+ hourCounts.set(i, 0);
1754
+ }
1755
+ for (const incident of incidents) {
1756
+ const hour = new Date(incident.timestamp).getHours();
1757
+ hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
1758
+ }
1759
+ return Array.from(hourCounts.entries()).map(([hour, count]) => ({
1760
+ hour,
1761
+ count
1762
+ }));
1763
+ }
1764
+ /**
1765
+ * Get incident rate by environment
1766
+ */
1767
+ getIncidentsByEnvironment() {
1768
+ const stats = this.getStats(30);
1769
+ const total = stats.incidents.total;
1770
+ return Object.entries(stats.incidents.byEnvironment).map(([environment, count]) => ({
1771
+ environment,
1772
+ count,
1773
+ percentage: total > 0 ? Math.round(count / total * 100) : 0
1774
+ })).sort((a, b) => b.count - a.count);
1775
+ }
1776
+ /**
1777
+ * Get symbol correlation matrix (which symbols fail together)
1778
+ */
1779
+ getSymbolCorrelation() {
1780
+ const incidents = this.storage.getRecentIncidents({ limit: 1e3 });
1781
+ const correlations = /* @__PURE__ */ new Map();
1782
+ const symbolCounts = /* @__PURE__ */ new Map();
1783
+ for (const incident of incidents) {
1784
+ const symbols = this.getSymbolsFromIncident(incident);
1785
+ for (const symbol of symbols) {
1786
+ symbolCounts.set(symbol, (symbolCounts.get(symbol) || 0) + 1);
1787
+ }
1788
+ for (let i = 0; i < symbols.length; i++) {
1789
+ for (let j = i + 1; j < symbols.length; j++) {
1790
+ const key = [symbols[i], symbols[j]].sort().join("|");
1791
+ correlations.set(key, (correlations.get(key) || 0) + 1);
1792
+ }
1793
+ }
1794
+ }
1795
+ const results = [];
1796
+ for (const [key, count] of correlations) {
1797
+ const [symbol1, symbol2] = key.split("|");
1798
+ const count1 = symbolCounts.get(symbol1) || 1;
1799
+ const count2 = symbolCounts.get(symbol2) || 1;
1800
+ const correlation = count / Math.max(count1, count2);
1801
+ if (correlation > 0.3) {
1802
+ results.push({
1803
+ symbol1,
1804
+ symbol2,
1805
+ correlation: Math.round(correlation * 100) / 100
1806
+ });
1807
+ }
1808
+ }
1809
+ return results.sort((a, b) => b.correlation - a.correlation).slice(0, 20);
1810
+ }
1811
+ /**
1812
+ * Generate a summary dashboard string
1813
+ */
1814
+ generateDashboard(periodDays = 7) {
1815
+ const stats = this.getStats(periodDays);
1816
+ const lines = [];
1817
+ 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");
1818
+ lines.push("\u2551 PARADIGM SENTINEL DASHBOARD \u2551");
1819
+ 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");
1820
+ const todayCount = stats.incidents.byDay[stats.incidents.byDay.length - 1]?.count || 0;
1821
+ lines.push(
1822
+ `\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`
1823
+ );
1824
+ 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");
1825
+ lines.push("");
1826
+ lines.push("Incidents by Day (last 7 days):");
1827
+ lines.push("\u2500".repeat(50));
1828
+ const maxDayCount = Math.max(...stats.incidents.byDay.map((d) => d.count), 1);
1829
+ for (const day of stats.incidents.byDay.slice(-7)) {
1830
+ const barLength = Math.round(day.count / maxDayCount * 30);
1831
+ const bar = "\u2588".repeat(barLength);
1832
+ lines.push(`${day.date.substring(5)} ${bar} ${day.count}`);
1833
+ }
1834
+ lines.push("");
1835
+ lines.push("Most Affected Symbols:");
1836
+ lines.push("\u2500".repeat(50));
1837
+ for (const { symbol, count } of stats.symbols.mostIncidents.slice(0, 5)) {
1838
+ lines.push(` ${symbol.padEnd(25)} ${count} incidents`);
1839
+ }
1840
+ lines.push("");
1841
+ lines.push("Top Patterns:");
1842
+ lines.push("\u2500".repeat(50));
1843
+ for (const { patternId, resolvedCount } of stats.patterns.mostEffective.slice(0, 5)) {
1844
+ lines.push(` ${patternId.padEnd(25)} ${resolvedCount} resolved`);
1845
+ }
1846
+ lines.push("");
1847
+ lines.push("Resolution Stats:");
1848
+ lines.push("\u2500".repeat(50));
1849
+ lines.push(` Resolution rate: ${Math.round(stats.resolution.resolutionRate)}%`);
1850
+ lines.push(` With pattern: ${stats.resolution.resolvedWithPattern}`);
1851
+ lines.push(` Manual: ${stats.resolution.resolvedManually}`);
1852
+ return lines.join("\n");
1853
+ }
1854
+ /**
1855
+ * Helper: Count symbols across incidents
1856
+ */
1857
+ countSymbols(incidents) {
1858
+ const counts = /* @__PURE__ */ new Map();
1859
+ for (const incident of incidents) {
1860
+ for (const [, value] of Object.entries(incident.symbols)) {
1861
+ if (value) {
1862
+ counts.set(value, (counts.get(value) || 0) + 1);
1863
+ }
1864
+ }
1865
+ }
1866
+ return counts;
1867
+ }
1868
+ /**
1869
+ * Helper: Get all symbols from incident
1870
+ */
1871
+ getSymbolsFromIncident(incident) {
1872
+ const symbols = [];
1873
+ for (const [, value] of Object.entries(incident.symbols)) {
1874
+ if (value) {
1875
+ symbols.push(value);
1876
+ }
1877
+ }
1878
+ return symbols;
1879
+ }
1880
+ };
1881
+
1882
+ // src/suggester.ts
1883
+ var PatternSuggester = class {
1884
+ constructor(storage2) {
1885
+ this.storage = storage2;
1886
+ }
1887
+ /**
1888
+ * Suggest a pattern from a resolved incident
1889
+ */
1890
+ suggestFromIncident(incident) {
1891
+ const baseId = this.generatePatternId(incident);
1892
+ const symbols = this.buildSymbolCriteria(incident.symbols);
1893
+ const errorKeywords = this.extractErrorKeywords(incident.error.message);
1894
+ const pattern = {
1895
+ id: baseId,
1896
+ name: this.generatePatternName(incident),
1897
+ description: `Auto-suggested pattern from incident ${incident.id}`,
1898
+ pattern: {
1899
+ symbols,
1900
+ errorContains: errorKeywords.length > 0 ? errorKeywords : void 0,
1901
+ missingSignals: incident.flowPosition?.missing
1902
+ },
1903
+ resolution: {
1904
+ description: incident.resolution?.notes || "Resolution approach TBD",
1905
+ strategy: "fix-code",
1906
+ priority: "medium"
1907
+ },
1908
+ source: "suggested",
1909
+ private: false,
1910
+ tags: this.generateTags(incident)
1911
+ };
1912
+ return pattern;
1913
+ }
1914
+ /**
1915
+ * Suggest a pattern from an incident group
1916
+ */
1917
+ suggestFromGroup(group) {
1918
+ const baseId = `group-${group.id.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
1919
+ const symbols = this.buildSymbolCriteria(group.commonSymbols);
1920
+ const pattern = {
1921
+ id: baseId,
1922
+ name: group.name || `Pattern from group ${group.id}`,
1923
+ description: `Auto-suggested pattern from incident group with ${group.count} incidents`,
1924
+ pattern: {
1925
+ symbols,
1926
+ errorContains: group.commonErrorPatterns.length > 0 ? group.commonErrorPatterns : void 0
1927
+ },
1928
+ resolution: {
1929
+ description: "Resolution approach TBD based on grouped incidents",
1930
+ strategy: "fix-code",
1931
+ priority: this.getPriorityFromCount(group.count)
1932
+ },
1933
+ source: "suggested",
1934
+ private: false,
1935
+ tags: this.generateTagsFromGroup(group)
1936
+ };
1937
+ return pattern;
1938
+ }
1939
+ /**
1940
+ * Find incidents that could become patterns
1941
+ */
1942
+ findPatternCandidates(minOccurrences = 3) {
1943
+ const incidents = this.storage.getRecentIncidents({
1944
+ limit: 1e3,
1945
+ status: "resolved"
1946
+ });
1947
+ const signatureGroups = /* @__PURE__ */ new Map();
1948
+ for (const incident of incidents) {
1949
+ const signature = this.getSymbolSignature(incident.symbols);
1950
+ const existing = signatureGroups.get(signature) || [];
1951
+ existing.push(incident);
1952
+ signatureGroups.set(signature, existing);
1953
+ }
1954
+ const candidates = [];
1955
+ for (const [, groupIncidents] of signatureGroups) {
1956
+ if (groupIncidents.length >= minOccurrences) {
1957
+ const hasPattern = this.hasMatchingPattern(groupIncidents[0]);
1958
+ if (hasPattern) continue;
1959
+ const suggestedPattern = this.suggestFromIncidents(groupIncidents);
1960
+ candidates.push({
1961
+ incidents: groupIncidents,
1962
+ suggestedPattern,
1963
+ occurrenceCount: groupIncidents.length
1964
+ });
1965
+ }
1966
+ }
1967
+ return candidates.sort((a, b) => b.occurrenceCount - a.occurrenceCount);
1968
+ }
1969
+ /**
1970
+ * Generate pattern from multiple similar incidents
1971
+ */
1972
+ suggestFromIncidents(incidents) {
1973
+ const commonSymbols = this.extractCommonSymbols(incidents);
1974
+ const symbols = this.buildSymbolCriteria(commonSymbols);
1975
+ const errorKeywords = this.extractCommonErrorKeywords(incidents);
1976
+ const missingSignals = this.extractCommonMissingSignals(incidents);
1977
+ const baseId = this.generatePatternId(incidents[0]);
1978
+ return {
1979
+ id: baseId,
1980
+ name: this.generatePatternName(incidents[0]),
1981
+ description: `Auto-suggested pattern from ${incidents.length} similar incidents`,
1982
+ pattern: {
1983
+ symbols,
1984
+ errorContains: errorKeywords.length > 0 ? errorKeywords : void 0,
1985
+ missingSignals: missingSignals.length > 0 ? missingSignals : void 0
1986
+ },
1987
+ resolution: {
1988
+ description: "Resolution approach based on previous resolutions",
1989
+ strategy: this.inferStrategy(incidents),
1990
+ priority: this.getPriorityFromCount(incidents.length)
1991
+ },
1992
+ source: "suggested",
1993
+ private: false,
1994
+ tags: this.generateTagsFromIncidents(incidents)
1995
+ };
1996
+ }
1997
+ /**
1998
+ * Build symbol criteria for pattern, adding wildcards where appropriate
1999
+ */
2000
+ buildSymbolCriteria(symbols) {
2001
+ const criteria = {};
2002
+ for (const [key, value] of Object.entries(symbols)) {
2003
+ if (value) {
2004
+ criteria[key] = value;
2005
+ }
2006
+ }
2007
+ return criteria;
2008
+ }
2009
+ /**
2010
+ * Extract keywords from error message
2011
+ */
2012
+ extractErrorKeywords(message) {
2013
+ const stopWords = /* @__PURE__ */ new Set([
2014
+ "the",
2015
+ "a",
2016
+ "an",
2017
+ "is",
2018
+ "are",
2019
+ "was",
2020
+ "were",
2021
+ "in",
2022
+ "on",
2023
+ "at",
2024
+ "to",
2025
+ "for",
2026
+ "of",
2027
+ "with",
2028
+ "and",
2029
+ "or",
2030
+ "but",
2031
+ "not",
2032
+ "no",
2033
+ "be",
2034
+ "been",
2035
+ "have",
2036
+ "has",
2037
+ "had",
2038
+ "do",
2039
+ "does",
2040
+ "did"
2041
+ ]);
2042
+ const words = message.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
2043
+ const unique = [...new Set(words)];
2044
+ return unique.slice(0, 5);
2045
+ }
2046
+ /**
2047
+ * Extract common error keywords from multiple incidents
2048
+ */
2049
+ extractCommonErrorKeywords(incidents) {
2050
+ const wordCounts = /* @__PURE__ */ new Map();
2051
+ for (const incident of incidents) {
2052
+ const keywords = this.extractErrorKeywords(incident.error.message);
2053
+ for (const keyword of keywords) {
2054
+ wordCounts.set(keyword, (wordCounts.get(keyword) || 0) + 1);
2055
+ }
2056
+ }
2057
+ const threshold = Math.ceil(incidents.length * 0.5);
2058
+ return Array.from(wordCounts.entries()).filter(([, count]) => count >= threshold).map(([word]) => word).slice(0, 5);
2059
+ }
2060
+ /**
2061
+ * Extract symbols common to all incidents
2062
+ */
2063
+ extractCommonSymbols(incidents) {
2064
+ if (incidents.length === 0) return {};
2065
+ const first = incidents[0].symbols;
2066
+ const common = {};
2067
+ for (const [key, value] of Object.entries(first)) {
2068
+ if (!value) continue;
2069
+ const allMatch = incidents.every(
2070
+ (i) => i.symbols[key] === value
2071
+ );
2072
+ if (allMatch) {
2073
+ common[key] = value;
2074
+ }
2075
+ }
2076
+ return common;
2077
+ }
2078
+ /**
2079
+ * Extract missing signals common to multiple incidents
2080
+ */
2081
+ extractCommonMissingSignals(incidents) {
2082
+ const signalCounts = /* @__PURE__ */ new Map();
2083
+ for (const incident of incidents) {
2084
+ if (!incident.flowPosition?.missing) continue;
2085
+ for (const signal of incident.flowPosition.missing) {
2086
+ signalCounts.set(signal, (signalCounts.get(signal) || 0) + 1);
2087
+ }
2088
+ }
2089
+ const threshold = Math.ceil(incidents.length * 0.5);
2090
+ return Array.from(signalCounts.entries()).filter(([, count]) => count >= threshold).map(([signal]) => signal);
2091
+ }
2092
+ /**
2093
+ * Generate a pattern ID from incident
2094
+ */
2095
+ generatePatternId(incident) {
2096
+ const parts = [];
2097
+ if (incident.symbols.gate) {
2098
+ parts.push(incident.symbols.gate.replace(/[^a-z0-9]/gi, ""));
2099
+ } else if (incident.symbols.feature) {
2100
+ parts.push(incident.symbols.feature.replace(/[^a-z0-9]/gi, ""));
2101
+ } else if (incident.symbols.component) {
2102
+ parts.push(incident.symbols.component.replace(/[^a-z0-9]/gi, ""));
2103
+ } else if (incident.symbols.integration) {
2104
+ parts.push(incident.symbols.integration.replace(/[^a-z0-9]/gi, ""));
2105
+ } else {
2106
+ parts.push("unknown");
2107
+ }
2108
+ const errorType = incident.error.type?.toLowerCase() || "error";
2109
+ parts.push(errorType.replace(/[^a-z0-9]/gi, ""));
2110
+ parts.push(String(Date.now() % 1e3).padStart(3, "0"));
2111
+ return parts.join("-");
2112
+ }
2113
+ /**
2114
+ * Generate a human-readable pattern name
2115
+ */
2116
+ generatePatternName(incident) {
2117
+ const parts = [];
2118
+ if (incident.symbols.feature) {
2119
+ parts.push(
2120
+ incident.symbols.feature.replace("@", "").replace(/-/g, " ")
2121
+ );
2122
+ }
2123
+ if (incident.symbols.gate) {
2124
+ parts.push("gate " + incident.symbols.gate.replace("^", ""));
2125
+ }
2126
+ if (incident.error.type) {
2127
+ parts.push(incident.error.type);
2128
+ }
2129
+ if (parts.length === 0) {
2130
+ return "Unnamed Pattern";
2131
+ }
2132
+ const name = parts.join(" - ");
2133
+ return name.charAt(0).toUpperCase() + name.slice(1);
2134
+ }
2135
+ /**
2136
+ * Generate tags from incident
2137
+ */
2138
+ generateTags(incident) {
2139
+ const tags = [];
2140
+ if (incident.symbols.feature) {
2141
+ tags.push("feature");
2142
+ }
2143
+ if (incident.symbols.gate) {
2144
+ tags.push("gate");
2145
+ }
2146
+ if (incident.symbols.integration) {
2147
+ tags.push("integration");
2148
+ tags.push(incident.symbols.integration.replace("&", ""));
2149
+ }
2150
+ if (incident.error.type) {
2151
+ tags.push(incident.error.type.toLowerCase());
2152
+ }
2153
+ tags.push(incident.environment);
2154
+ return [...new Set(tags)].slice(0, 5);
2155
+ }
2156
+ /**
2157
+ * Generate tags from incident group
2158
+ */
2159
+ generateTagsFromGroup(group) {
2160
+ const tags = ["grouped"];
2161
+ if (group.commonSymbols.feature) {
2162
+ tags.push("feature");
2163
+ }
2164
+ if (group.commonSymbols.gate) {
2165
+ tags.push("gate");
2166
+ }
2167
+ if (group.commonSymbols.integration) {
2168
+ tags.push("integration");
2169
+ }
2170
+ for (const env of group.environments) {
2171
+ tags.push(env);
2172
+ }
2173
+ return [...new Set(tags)].slice(0, 5);
2174
+ }
2175
+ /**
2176
+ * Generate tags from multiple incidents
2177
+ */
2178
+ generateTagsFromIncidents(incidents) {
2179
+ const tagCounts = /* @__PURE__ */ new Map();
2180
+ for (const incident of incidents) {
2181
+ const tags = this.generateTags(incident);
2182
+ for (const tag of tags) {
2183
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
2184
+ }
2185
+ }
2186
+ return Array.from(tagCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([tag]) => tag);
2187
+ }
2188
+ /**
2189
+ * Get symbol signature for grouping
2190
+ */
2191
+ getSymbolSignature(symbols) {
2192
+ const parts = [];
2193
+ if (symbols.feature) parts.push(`f:${symbols.feature}`);
2194
+ if (symbols.component) parts.push(`c:${symbols.component}`);
2195
+ if (symbols.flow) parts.push(`fl:${symbols.flow}`);
2196
+ if (symbols.gate) parts.push(`g:${symbols.gate}`);
2197
+ if (symbols.integration) parts.push(`i:${symbols.integration}`);
2198
+ return parts.sort().join("|");
2199
+ }
2200
+ /**
2201
+ * Check if there's already a pattern matching this incident
2202
+ */
2203
+ hasMatchingPattern(incident) {
2204
+ const patterns = this.storage.getAllPatterns({ includePrivate: true });
2205
+ for (const pattern of patterns) {
2206
+ let matchCount = 0;
2207
+ const symbolTypes = [
2208
+ "feature",
2209
+ "component",
2210
+ "flow",
2211
+ "gate",
2212
+ "signal",
2213
+ "integration"
2214
+ ];
2215
+ for (const type of symbolTypes) {
2216
+ const patternValue = pattern.pattern.symbols[type];
2217
+ const incidentValue = incident.symbols[type];
2218
+ if (patternValue && incidentValue && patternValue === incidentValue) {
2219
+ matchCount++;
2220
+ }
2221
+ }
2222
+ if (matchCount >= 2) {
2223
+ return true;
2224
+ }
2225
+ }
2226
+ return false;
2227
+ }
2228
+ /**
2229
+ * Infer resolution strategy from incidents
2230
+ */
2231
+ inferStrategy(incidents) {
2232
+ const messages = incidents.map((i) => i.error.message.toLowerCase());
2233
+ if (messages.some((m) => m.includes("timeout") || m.includes("network"))) {
2234
+ return "retry";
2235
+ }
2236
+ if (messages.some(
2237
+ (m) => m.includes("validation") || m.includes("invalid") || m.includes("required")
2238
+ )) {
2239
+ return "fix-data";
2240
+ }
2241
+ if (messages.some((m) => m.includes("permission") || m.includes("403"))) {
2242
+ return "escalate";
2243
+ }
2244
+ return "fix-code";
2245
+ }
2246
+ /**
2247
+ * Get priority based on occurrence count
2248
+ */
2249
+ getPriorityFromCount(count) {
2250
+ if (count >= 20) return "critical";
2251
+ if (count >= 10) return "high";
2252
+ if (count >= 5) return "medium";
2253
+ return "low";
2254
+ }
2255
+ };
2256
+
2257
+ // src/seeds/loader.ts
2258
+ import * as path2 from "path";
2259
+ import * as fs2 from "fs";
2260
+ import { fileURLToPath } from "url";
2261
+ var __filename = fileURLToPath(import.meta.url);
2262
+ var __dirname = path2.dirname(__filename);
2263
+ function loadUniversalPatterns() {
2264
+ const filePath = path2.join(__dirname, "universal-patterns.json");
2265
+ const content = fs2.readFileSync(filePath, "utf-8");
2266
+ return JSON.parse(content);
2267
+ }
2268
+ function loadParadigmPatterns() {
2269
+ const filePath = path2.join(__dirname, "paradigm-patterns.json");
2270
+ const content = fs2.readFileSync(filePath, "utf-8");
2271
+ return JSON.parse(content);
2272
+ }
2273
+ function loadAllSeedPatterns() {
2274
+ const universal = loadUniversalPatterns();
2275
+ const paradigm = loadParadigmPatterns();
2276
+ return {
2277
+ version: "1.0.0",
2278
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2279
+ patterns: [...universal.patterns, ...paradigm.patterns]
2280
+ };
2281
+ }
2282
+
2283
+ // src/mcp.ts
2284
+ var storage = null;
2285
+ var initialized = false;
2286
+ function getStorage() {
2287
+ if (!storage) {
2288
+ storage = new SentinelStorage();
2289
+ }
2290
+ if (!initialized) {
2291
+ try {
2292
+ const { patterns } = loadAllSeedPatterns();
2293
+ for (const pattern of patterns) {
2294
+ try {
2295
+ storage.addPattern(pattern);
2296
+ } catch {
2297
+ }
2298
+ }
2299
+ } catch {
2300
+ }
2301
+ initialized = true;
2302
+ }
2303
+ return storage;
2304
+ }
2305
+ function getToolsList() {
2306
+ return [
2307
+ {
2308
+ name: "sentinel_triage",
2309
+ description: "View and filter incidents with pattern matches. Returns recent errors with symbolic context and resolution suggestions.",
2310
+ annotations: {
2311
+ readOnlyHint: true,
2312
+ destructiveHint: false
2313
+ },
2314
+ inputSchema: {
2315
+ type: "object",
2316
+ properties: {
2317
+ symbol: { type: "string", description: "Filter by symbol (e.g., #checkout, ^auth)" },
2318
+ status: {
2319
+ type: "string",
2320
+ enum: ["open", "investigating", "resolved", "wont-fix", "all"],
2321
+ description: "Filter by status (default: all)"
2322
+ },
2323
+ environment: { type: "string", description: "Filter by environment" },
2324
+ search: { type: "string", description: "Search in error messages" },
2325
+ limit: { type: "number", description: "Max results (default: 10)" }
2326
+ }
2327
+ }
2328
+ },
2329
+ {
2330
+ name: "sentinel_show",
2331
+ description: "Get full details of a specific incident including timeline and matched patterns.",
2332
+ annotations: {
2333
+ readOnlyHint: true,
2334
+ destructiveHint: false
2335
+ },
2336
+ inputSchema: {
2337
+ type: "object",
2338
+ properties: {
2339
+ incidentId: { type: "string", description: "Incident ID (e.g., INC-001)" },
2340
+ includeTimeline: { type: "boolean", description: "Include flow timeline" },
2341
+ includeSimilar: { type: "boolean", description: "Include similar incidents" }
2342
+ },
2343
+ required: ["incidentId"]
2344
+ }
2345
+ },
2346
+ {
2347
+ name: "sentinel_resolve",
2348
+ description: "Mark an incident as resolved with optional pattern and commit reference.",
2349
+ annotations: {
2350
+ readOnlyHint: false,
2351
+ destructiveHint: false
2352
+ },
2353
+ inputSchema: {
2354
+ type: "object",
2355
+ properties: {
2356
+ incidentId: { type: "string", description: "Incident ID" },
2357
+ patternId: { type: "string", description: "Pattern that led to resolution" },
2358
+ commitHash: { type: "string", description: "Fix commit hash" },
2359
+ prUrl: { type: "string", description: "PR URL" },
2360
+ notes: { type: "string", description: "Resolution notes" },
2361
+ wontFix: { type: "boolean", description: "Mark as wont-fix instead of resolved" }
2362
+ },
2363
+ required: ["incidentId"]
2364
+ }
2365
+ },
2366
+ {
2367
+ name: "sentinel_patterns",
2368
+ description: "List and filter failure patterns with confidence scores.",
2369
+ annotations: {
2370
+ readOnlyHint: true,
2371
+ destructiveHint: false
2372
+ },
2373
+ inputSchema: {
2374
+ type: "object",
2375
+ properties: {
2376
+ symbol: { type: "string", description: "Filter patterns for this symbol" },
2377
+ minConfidence: { type: "number", description: "Minimum confidence score" },
2378
+ source: {
2379
+ type: "string",
2380
+ enum: ["manual", "suggested", "imported", "community"],
2381
+ description: "Filter by source"
2382
+ }
2383
+ }
2384
+ }
2385
+ },
2386
+ {
2387
+ name: "sentinel_add_pattern",
2388
+ description: "Create a new failure pattern.",
2389
+ annotations: {
2390
+ readOnlyHint: false,
2391
+ destructiveHint: false
2392
+ },
2393
+ inputSchema: {
2394
+ type: "object",
2395
+ properties: {
2396
+ id: { type: "string", description: "Pattern ID (kebab-case)" },
2397
+ name: { type: "string", description: "Human-readable name" },
2398
+ description: { type: "string", description: "What this pattern matches" },
2399
+ pattern: {
2400
+ type: "object",
2401
+ properties: {
2402
+ symbols: { type: "object", description: "Symbol criteria" },
2403
+ errorContains: { type: "array", items: { type: "string" }, description: "Error keywords" },
2404
+ missingSignals: { type: "array", items: { type: "string" }, description: "Expected missing signals" }
2405
+ }
2406
+ },
2407
+ resolution: {
2408
+ type: "object",
2409
+ properties: {
2410
+ description: { type: "string", description: "Resolution steps" },
2411
+ strategy: {
2412
+ type: "string",
2413
+ enum: ["retry", "fallback", "fix-data", "fix-code", "ignore", "escalate"]
2414
+ },
2415
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
2416
+ codeHint: { type: "string", description: "Code fix hint" }
2417
+ },
2418
+ required: ["description", "strategy"]
2419
+ },
2420
+ tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" }
2421
+ },
2422
+ required: ["id", "name", "pattern", "resolution"]
2423
+ }
2424
+ },
2425
+ {
2426
+ name: "sentinel_record",
2427
+ description: "Manually record a new incident.",
2428
+ annotations: {
2429
+ readOnlyHint: false,
2430
+ destructiveHint: false
2431
+ },
2432
+ inputSchema: {
2433
+ type: "object",
2434
+ properties: {
2435
+ error: {
2436
+ type: "object",
2437
+ properties: {
2438
+ message: { type: "string", description: "Error message" },
2439
+ stack: { type: "string", description: "Stack trace" },
2440
+ code: { type: "string", description: "Error code" },
2441
+ type: { type: "string", description: "Error type" }
2442
+ },
2443
+ required: ["message"]
2444
+ },
2445
+ symbols: {
2446
+ type: "object",
2447
+ properties: {
2448
+ component: { type: "string" },
2449
+ flow: { type: "string" },
2450
+ gate: { type: "string" },
2451
+ signal: { type: "string" }
2452
+ }
2453
+ },
2454
+ environment: { type: "string", description: "Environment (required)" },
2455
+ service: { type: "string", description: "Service name" },
2456
+ version: { type: "string", description: "App version" },
2457
+ flowPosition: {
2458
+ type: "object",
2459
+ properties: {
2460
+ flowId: { type: "string" },
2461
+ expected: { type: "array", items: { type: "string" } },
2462
+ actual: { type: "array", items: { type: "string" } },
2463
+ missing: { type: "array", items: { type: "string" } },
2464
+ failedAt: { type: "string" }
2465
+ }
2466
+ }
2467
+ },
2468
+ required: ["error", "symbols", "environment"]
2469
+ }
2470
+ },
2471
+ {
2472
+ name: "sentinel_stats",
2473
+ description: "Get statistics and health metrics.",
2474
+ annotations: {
2475
+ readOnlyHint: true,
2476
+ destructiveHint: false
2477
+ },
2478
+ inputSchema: {
2479
+ type: "object",
2480
+ properties: {
2481
+ period: { type: "string", description: "Period: 1d, 7d, 30d, 90d (default: 7d)" },
2482
+ symbol: { type: "string", description: "Get health for specific symbol" }
2483
+ }
2484
+ }
2485
+ },
2486
+ {
2487
+ name: "sentinel_suggest_pattern",
2488
+ description: "Get AI suggestions for patterns based on incidents.",
2489
+ annotations: {
2490
+ readOnlyHint: true,
2491
+ destructiveHint: false
2492
+ },
2493
+ inputSchema: {
2494
+ type: "object",
2495
+ properties: {
2496
+ incidentId: { type: "string", description: "Suggest from specific incident" },
2497
+ minOccurrences: { type: "number", description: "Min similar incidents for suggestion" }
2498
+ }
2499
+ }
2500
+ }
2501
+ ];
2502
+ }
2503
+ async function handleTool(name, args) {
2504
+ const store = getStorage();
2505
+ const matcher = new PatternMatcher(store);
2506
+ switch (name) {
2507
+ case "sentinel_triage": {
2508
+ const { symbol, status = "all", environment, search, limit = 10 } = args;
2509
+ const incidents = store.getRecentIncidents({
2510
+ limit,
2511
+ status,
2512
+ symbol,
2513
+ environment,
2514
+ search
2515
+ });
2516
+ if (incidents.length === 0) {
2517
+ return JSON.stringify({
2518
+ count: 0,
2519
+ incidents: [],
2520
+ tip: "No incidents recorded yet. Use sentinel_record to create incidents or integrate the SDK."
2521
+ }, null, 2);
2522
+ }
2523
+ const results = incidents.map((incident) => {
2524
+ const matches = matcher.match(incident, { maxResults: 3 });
2525
+ return {
2526
+ id: incident.id,
2527
+ timestamp: incident.timestamp,
2528
+ status: incident.status,
2529
+ error: incident.error.message,
2530
+ symbols: incident.symbols,
2531
+ environment: incident.environment,
2532
+ matches: matches.map((m) => ({
2533
+ patternId: m.pattern.id,
2534
+ name: m.pattern.name,
2535
+ confidence: m.confidence,
2536
+ strategy: m.pattern.resolution.strategy,
2537
+ description: m.pattern.resolution.description
2538
+ }))
2539
+ };
2540
+ });
2541
+ return JSON.stringify({ count: results.length, incidents: results }, null, 2);
2542
+ }
2543
+ case "sentinel_show": {
2544
+ const { incidentId, includeTimeline, includeSimilar } = args;
2545
+ const incident = store.getIncident(incidentId);
2546
+ if (!incident) {
2547
+ return JSON.stringify({ error: `Incident ${incidentId} not found` });
2548
+ }
2549
+ const matches = matcher.match(incident, { maxResults: 5 });
2550
+ const result = {
2551
+ incident,
2552
+ matches: matches.map((m) => ({
2553
+ patternId: m.pattern.id,
2554
+ name: m.pattern.name,
2555
+ confidence: m.confidence,
2556
+ matchedCriteria: m.matchedCriteria,
2557
+ resolution: m.pattern.resolution
2558
+ }))
2559
+ };
2560
+ if (includeTimeline && incident.flowPosition) {
2561
+ const timeline = new TimelineBuilder().build(incident);
2562
+ if (timeline) {
2563
+ result.timeline = new TimelineBuilder().renderStructured(timeline);
2564
+ }
2565
+ }
2566
+ if (includeSimilar) {
2567
+ const similar = store.getRecentIncidents({ symbol: Object.values(incident.symbols)[0], limit: 5 }).filter((i) => i.id !== incidentId);
2568
+ result.similar = similar.map((i) => ({
2569
+ id: i.id,
2570
+ error: i.error.message,
2571
+ status: i.status
2572
+ }));
2573
+ }
2574
+ return JSON.stringify(result, null, 2);
2575
+ }
2576
+ case "sentinel_resolve": {
2577
+ const { incidentId, patternId, commitHash, prUrl, notes, wontFix } = args;
2578
+ const incident = store.getIncident(incidentId);
2579
+ if (!incident) {
2580
+ return JSON.stringify({ error: `Incident ${incidentId} not found` });
2581
+ }
2582
+ if (wontFix) {
2583
+ store.updateIncident(incidentId, {
2584
+ status: "wont-fix",
2585
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
2586
+ resolvedBy: "manual",
2587
+ resolution: { notes }
2588
+ });
2589
+ return JSON.stringify({
2590
+ success: true,
2591
+ message: `Incident ${incidentId} marked as won't fix`
2592
+ });
2593
+ }
2594
+ store.recordResolution({ incidentId, patternId, commitHash, prUrl, notes });
2595
+ return JSON.stringify({
2596
+ success: true,
2597
+ message: `Incident ${incidentId} resolved`,
2598
+ patternId,
2599
+ commitHash,
2600
+ prUrl
2601
+ });
2602
+ }
2603
+ case "sentinel_patterns": {
2604
+ const { symbol, minConfidence, source } = args;
2605
+ const patterns = store.getAllPatterns({
2606
+ source,
2607
+ minConfidence,
2608
+ includePrivate: false
2609
+ });
2610
+ let filtered = patterns;
2611
+ if (symbol) {
2612
+ filtered = patterns.filter((p) => {
2613
+ const symbols = p.pattern.symbols;
2614
+ return Object.values(symbols).some((v) => {
2615
+ if (!v) return false;
2616
+ if (Array.isArray(v)) return v.includes(symbol) || v.some((s) => symbol.match(s.replace("*", ".*")));
2617
+ return v === symbol || symbol.match(v.replace("*", ".*"));
2618
+ });
2619
+ });
2620
+ }
2621
+ return JSON.stringify(
2622
+ {
2623
+ count: filtered.length,
2624
+ patterns: filtered.map((p) => ({
2625
+ id: p.id,
2626
+ name: p.name,
2627
+ description: p.description,
2628
+ confidence: p.confidence.score,
2629
+ resolution: p.resolution,
2630
+ tags: p.tags
2631
+ }))
2632
+ },
2633
+ null,
2634
+ 2
2635
+ );
2636
+ }
2637
+ case "sentinel_add_pattern": {
2638
+ const { id, name: name2, description, pattern, resolution, tags } = args;
2639
+ const input = {
2640
+ id,
2641
+ name: name2,
2642
+ description: description || "",
2643
+ pattern: {
2644
+ symbols: pattern.symbols || {},
2645
+ errorContains: pattern.errorContains,
2646
+ missingSignals: pattern.missingSignals
2647
+ },
2648
+ resolution: {
2649
+ description: resolution.description,
2650
+ strategy: resolution.strategy,
2651
+ priority: resolution.priority || "medium",
2652
+ codeHint: resolution.codeHint
2653
+ },
2654
+ source: "manual",
2655
+ private: false,
2656
+ tags: tags || []
2657
+ };
2658
+ store.addPattern(input);
2659
+ return JSON.stringify({
2660
+ success: true,
2661
+ message: `Pattern ${id} created`,
2662
+ pattern: input
2663
+ });
2664
+ }
2665
+ case "sentinel_record": {
2666
+ const { error, symbols, environment, service, version, flowPosition } = args;
2667
+ const incidentId = store.recordIncident({
2668
+ error,
2669
+ symbols,
2670
+ environment,
2671
+ service,
2672
+ version,
2673
+ flowPosition
2674
+ });
2675
+ const incident = store.getIncident(incidentId);
2676
+ const matches = incident ? matcher.match(incident, { maxResults: 3 }) : [];
2677
+ return JSON.stringify(
2678
+ {
2679
+ success: true,
2680
+ incidentId,
2681
+ matches: matches.map((m) => ({
2682
+ patternId: m.pattern.id,
2683
+ confidence: m.confidence,
2684
+ resolution: m.pattern.resolution.description
2685
+ }))
2686
+ },
2687
+ null,
2688
+ 2
2689
+ );
2690
+ }
2691
+ case "sentinel_stats": {
2692
+ const { period = "7d", symbol } = args;
2693
+ const calculator = new StatsCalculator(store);
2694
+ if (symbol) {
2695
+ const health = calculator.getSymbolHealth(symbol);
2696
+ return JSON.stringify({ symbol, health }, null, 2);
2697
+ }
2698
+ const match = period.match(/^(\d+)d$/);
2699
+ const periodDays = match ? parseInt(match[1], 10) : 7;
2700
+ const stats = calculator.getStats(periodDays);
2701
+ return JSON.stringify({ period: `${periodDays}d`, stats }, null, 2);
2702
+ }
2703
+ case "sentinel_suggest_pattern": {
2704
+ const { incidentId, minOccurrences } = args;
2705
+ const suggester = new PatternSuggester(store);
2706
+ if (incidentId) {
2707
+ const incident = store.getIncident(incidentId);
2708
+ if (!incident) {
2709
+ return JSON.stringify({ error: `Incident ${incidentId} not found` });
2710
+ }
2711
+ const suggestion = suggester.suggestFromIncident(incident);
2712
+ return JSON.stringify({ source: "incident", incidentId, suggestion }, null, 2);
2713
+ }
2714
+ const candidates = suggester.findPatternCandidates(minOccurrences || 3);
2715
+ return JSON.stringify(
2716
+ {
2717
+ source: "analysis",
2718
+ candidates: candidates.slice(0, 5).map((c) => ({
2719
+ occurrences: c.occurrenceCount,
2720
+ sampleIncidents: c.incidents.slice(0, 3).map((i) => i.id),
2721
+ suggestion: c.suggestedPattern
2722
+ }))
2723
+ },
2724
+ null,
2725
+ 2
2726
+ );
2727
+ }
2728
+ default:
2729
+ return JSON.stringify({ error: `Unknown tool: ${name}` });
2730
+ }
2731
+ }
2732
+ async function main() {
2733
+ console.error("[sentinel-mcp] Starting...");
2734
+ const store = getStorage();
2735
+ await store.ensureReady();
2736
+ console.error("[sentinel-mcp] Storage initialized");
2737
+ const server = new Server(
2738
+ { name: "sentinel", version: "0.2.0" },
2739
+ { capabilities: { tools: {} } }
2740
+ );
2741
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2742
+ tools: getToolsList()
2743
+ }));
2744
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2745
+ const { name, arguments: args } = request.params;
2746
+ try {
2747
+ const text = await handleTool(name, args || {});
2748
+ return { content: [{ type: "text", text }] };
2749
+ } catch (error) {
2750
+ const message = error instanceof Error ? error.message : String(error);
2751
+ return {
2752
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
2753
+ isError: true
2754
+ };
2755
+ }
2756
+ });
2757
+ server.onerror = (error) => {
2758
+ console.error("[sentinel-mcp] Server error:", error);
2759
+ };
2760
+ const transport = new StdioServerTransport();
2761
+ await server.connect(transport);
2762
+ console.error("[sentinel-mcp] Running on stdio");
2763
+ }
2764
+ main().catch((err) => {
2765
+ console.error("[sentinel-mcp] Fatal:", err);
2766
+ process.exit(1);
2767
+ });