@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.
@@ -0,0 +1,1249 @@
1
+ // src/storage.ts
2
+ import initSqlJs from "sql.js";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import * as path from "path";
5
+ import * as fs from "fs";
6
+ var SCHEMA_VERSION = 2;
7
+ var DEFAULT_CONFIDENCE = {
8
+ score: 50,
9
+ timesMatched: 0,
10
+ timesResolved: 0,
11
+ timesRecurred: 0
12
+ };
13
+ var SQL = null;
14
+ var SentinelStorage = class {
15
+ db = null;
16
+ dbPath;
17
+ incidentCounter = 0;
18
+ initialized = false;
19
+ constructor(dbPath) {
20
+ this.dbPath = dbPath || this.getDefaultDbPath();
21
+ }
22
+ getDefaultDbPath() {
23
+ const dataDir = process.env.SENTINEL_DATA_DIR || process.env.PARADIGM_DATA_DIR || path.join(process.cwd(), ".paradigm", "sentinel");
24
+ return path.join(dataDir, "sentinel.db");
25
+ }
26
+ createSchema() {
27
+ if (!this.db) return;
28
+ this.db.run(`
29
+ -- Metadata table for schema versioning
30
+ CREATE TABLE IF NOT EXISTS metadata (
31
+ key TEXT PRIMARY KEY,
32
+ value TEXT NOT NULL
33
+ );
34
+
35
+ -- Incidents table
36
+ CREATE TABLE IF NOT EXISTS incidents (
37
+ id TEXT PRIMARY KEY,
38
+ timestamp TEXT NOT NULL,
39
+ status TEXT NOT NULL DEFAULT 'open',
40
+ error_message TEXT NOT NULL,
41
+ error_stack TEXT,
42
+ error_code TEXT,
43
+ error_type TEXT,
44
+ symbols TEXT NOT NULL,
45
+ flow_position TEXT,
46
+ environment TEXT NOT NULL,
47
+ service TEXT,
48
+ version TEXT,
49
+ user_id TEXT,
50
+ request_id TEXT,
51
+ group_id TEXT,
52
+ notes TEXT DEFAULT '[]',
53
+ related_incidents TEXT DEFAULT '[]',
54
+ resolved_at TEXT,
55
+ resolved_by TEXT,
56
+ resolution TEXT,
57
+ created_at TEXT NOT NULL,
58
+ updated_at TEXT NOT NULL
59
+ );
60
+
61
+ -- Patterns table
62
+ CREATE TABLE IF NOT EXISTS patterns (
63
+ id TEXT PRIMARY KEY,
64
+ name TEXT NOT NULL,
65
+ description TEXT,
66
+ pattern TEXT NOT NULL,
67
+ resolution TEXT NOT NULL,
68
+ confidence TEXT NOT NULL,
69
+ source TEXT NOT NULL DEFAULT 'manual',
70
+ private INTEGER NOT NULL DEFAULT 0,
71
+ tags TEXT DEFAULT '[]',
72
+ created_at TEXT NOT NULL,
73
+ updated_at TEXT NOT NULL
74
+ );
75
+
76
+ -- Incident groups
77
+ CREATE TABLE IF NOT EXISTS groups (
78
+ id TEXT PRIMARY KEY,
79
+ name TEXT,
80
+ common_symbols TEXT,
81
+ common_error_patterns TEXT,
82
+ suggested_pattern_id TEXT,
83
+ first_seen TEXT NOT NULL,
84
+ last_seen TEXT NOT NULL,
85
+ created_at TEXT NOT NULL,
86
+ updated_at TEXT NOT NULL
87
+ );
88
+
89
+ -- Group members
90
+ CREATE TABLE IF NOT EXISTS group_members (
91
+ group_id TEXT NOT NULL,
92
+ incident_id TEXT NOT NULL,
93
+ added_at TEXT NOT NULL,
94
+ PRIMARY KEY (group_id, incident_id)
95
+ );
96
+
97
+ -- Resolutions history
98
+ CREATE TABLE IF NOT EXISTS resolutions (
99
+ id TEXT PRIMARY KEY,
100
+ incident_id TEXT NOT NULL,
101
+ pattern_id TEXT,
102
+ commit_hash TEXT,
103
+ pr_url TEXT,
104
+ notes TEXT,
105
+ resolved_at TEXT NOT NULL,
106
+ recurred INTEGER DEFAULT 0
107
+ );
108
+
109
+ -- Practice events (habits system)
110
+ CREATE TABLE IF NOT EXISTS practice_events (
111
+ id TEXT PRIMARY KEY,
112
+ timestamp TEXT NOT NULL,
113
+ habit_id TEXT NOT NULL,
114
+ habit_category TEXT NOT NULL,
115
+ result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
116
+ engineer TEXT NOT NULL,
117
+ session_id TEXT NOT NULL,
118
+ lore_entry_id TEXT,
119
+ task_description TEXT,
120
+ symbols_touched TEXT DEFAULT '[]',
121
+ files_modified TEXT DEFAULT '[]',
122
+ related_incident_id TEXT,
123
+ notes TEXT
124
+ );
125
+
126
+ -- Indexes
127
+ CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
128
+ CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
129
+ CREATE INDEX IF NOT EXISTS idx_incidents_environment ON incidents(environment);
130
+ CREATE INDEX IF NOT EXISTS idx_patterns_source ON patterns(source);
131
+ CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
132
+ CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
133
+ CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
134
+ CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
135
+ `);
136
+ this.db.run(
137
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
138
+ [String(SCHEMA_VERSION)]
139
+ );
140
+ }
141
+ save() {
142
+ if (!this.db) return;
143
+ const data = this.db.export();
144
+ const buffer = Buffer.from(data);
145
+ fs.writeFileSync(this.dbPath, buffer);
146
+ }
147
+ // ─── Incidents ───────────────────────────────────────────────────
148
+ recordIncident(input) {
149
+ const db = this.db;
150
+ if (!db) {
151
+ this.initializeSync();
152
+ }
153
+ this.incidentCounter++;
154
+ const id = `INC-${String(this.incidentCounter).padStart(3, "0")}`;
155
+ const now = (/* @__PURE__ */ new Date()).toISOString();
156
+ this.db.run(
157
+ `INSERT INTO incidents (
158
+ id, timestamp, status, error_message, error_stack, error_code, error_type,
159
+ symbols, flow_position, environment, service, version, user_id, request_id,
160
+ group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
161
+ created_at, updated_at
162
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
163
+ [
164
+ id,
165
+ input.timestamp || now,
166
+ input.status || "open",
167
+ input.error.message,
168
+ input.error.stack || null,
169
+ input.error.code || null,
170
+ input.error.type || null,
171
+ JSON.stringify(input.symbols),
172
+ input.flowPosition ? JSON.stringify(input.flowPosition) : null,
173
+ input.environment,
174
+ input.service || null,
175
+ input.version || null,
176
+ input.userId || null,
177
+ input.requestId || null,
178
+ input.groupId || null,
179
+ "[]",
180
+ "[]",
181
+ input.resolvedAt || null,
182
+ input.resolvedBy || null,
183
+ input.resolution ? JSON.stringify(input.resolution) : null,
184
+ now,
185
+ now
186
+ ]
187
+ );
188
+ this.save();
189
+ return id;
190
+ }
191
+ initializeSync() {
192
+ if (this.initialized && this.db) return;
193
+ if (!this.db && SQL) {
194
+ const dir = path.dirname(this.dbPath);
195
+ if (!fs.existsSync(dir)) {
196
+ fs.mkdirSync(dir, { recursive: true });
197
+ }
198
+ if (fs.existsSync(this.dbPath)) {
199
+ const fileData = fs.readFileSync(this.dbPath);
200
+ this.db = new SQL.Database(fileData);
201
+ } else {
202
+ this.db = new SQL.Database();
203
+ this.createSchema();
204
+ }
205
+ try {
206
+ const result = this.db.exec(
207
+ "SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
208
+ );
209
+ if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
210
+ this.incidentCounter = result[0].values[0][0];
211
+ }
212
+ } catch {
213
+ this.incidentCounter = 0;
214
+ }
215
+ this.migrateSchema();
216
+ this.initialized = true;
217
+ this.save();
218
+ }
219
+ }
220
+ /**
221
+ * Run schema migrations from older versions
222
+ */
223
+ migrateSchema() {
224
+ if (!this.db) return;
225
+ let currentVersion = 1;
226
+ try {
227
+ const result = this.db.exec(
228
+ "SELECT value FROM metadata WHERE key = 'schema_version'"
229
+ );
230
+ if (result.length > 0 && result[0].values.length > 0) {
231
+ currentVersion = parseInt(result[0].values[0][0], 10) || 1;
232
+ }
233
+ } catch {
234
+ }
235
+ if (currentVersion < 2) {
236
+ try {
237
+ this.db.run(`
238
+ CREATE TABLE IF NOT EXISTS practice_events (
239
+ id TEXT PRIMARY KEY,
240
+ timestamp TEXT NOT NULL,
241
+ habit_id TEXT NOT NULL,
242
+ habit_category TEXT NOT NULL,
243
+ result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
244
+ engineer TEXT NOT NULL,
245
+ session_id TEXT NOT NULL,
246
+ lore_entry_id TEXT,
247
+ task_description TEXT,
248
+ symbols_touched TEXT DEFAULT '[]',
249
+ files_modified TEXT DEFAULT '[]',
250
+ related_incident_id TEXT,
251
+ notes TEXT
252
+ );
253
+
254
+ CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
255
+ CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
256
+ CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
257
+ CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
258
+ `);
259
+ } catch {
260
+ }
261
+ this.db.run(
262
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
263
+ );
264
+ }
265
+ }
266
+ /**
267
+ * Ensure the storage is ready for use. Must be called once before using storage methods.
268
+ */
269
+ async ensureReady() {
270
+ if (!SQL) {
271
+ SQL = await initSqlJs();
272
+ }
273
+ this.initializeSync();
274
+ }
275
+ getIncident(id) {
276
+ this.initializeSync();
277
+ const result = this.db.exec("SELECT * FROM incidents WHERE id = ?", [id]);
278
+ if (result.length === 0 || result[0].values.length === 0) return null;
279
+ return this.rowToIncident(result[0].columns, result[0].values[0]);
280
+ }
281
+ getRecentIncidents(options = {}) {
282
+ this.initializeSync();
283
+ const { limit = 50, offset = 0 } = options;
284
+ const conditions = [];
285
+ const params = [];
286
+ if (options.status && options.status !== "all") {
287
+ conditions.push("status = ?");
288
+ params.push(options.status);
289
+ }
290
+ if (options.environment) {
291
+ conditions.push("environment = ?");
292
+ params.push(options.environment);
293
+ }
294
+ if (options.symbol) {
295
+ conditions.push("symbols LIKE ?");
296
+ params.push(`%${options.symbol}%`);
297
+ }
298
+ if (options.search) {
299
+ conditions.push("(error_message LIKE ? OR notes LIKE ?)");
300
+ params.push(`%${options.search}%`, `%${options.search}%`);
301
+ }
302
+ if (options.dateFrom) {
303
+ conditions.push("timestamp >= ?");
304
+ params.push(options.dateFrom);
305
+ }
306
+ if (options.dateTo) {
307
+ conditions.push("timestamp <= ?");
308
+ params.push(options.dateTo);
309
+ }
310
+ if (options.groupId) {
311
+ conditions.push("group_id = ?");
312
+ params.push(options.groupId);
313
+ }
314
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
315
+ const result = this.db.exec(
316
+ `SELECT * FROM incidents ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
317
+ [...params, limit, offset]
318
+ );
319
+ if (result.length === 0) return [];
320
+ return result[0].values.map(
321
+ (row) => this.rowToIncident(result[0].columns, row)
322
+ );
323
+ }
324
+ updateIncident(id, updates) {
325
+ this.initializeSync();
326
+ const now = (/* @__PURE__ */ new Date()).toISOString();
327
+ const setClauses = ["updated_at = ?"];
328
+ const params = [now];
329
+ if (updates.status !== void 0) {
330
+ setClauses.push("status = ?");
331
+ params.push(updates.status);
332
+ }
333
+ if (updates.error !== void 0) {
334
+ setClauses.push("error_message = ?");
335
+ params.push(updates.error.message);
336
+ if (updates.error.stack !== void 0) {
337
+ setClauses.push("error_stack = ?");
338
+ params.push(updates.error.stack || null);
339
+ }
340
+ }
341
+ if (updates.symbols !== void 0) {
342
+ setClauses.push("symbols = ?");
343
+ params.push(JSON.stringify(updates.symbols));
344
+ }
345
+ if (updates.flowPosition !== void 0) {
346
+ setClauses.push("flow_position = ?");
347
+ params.push(
348
+ updates.flowPosition ? JSON.stringify(updates.flowPosition) : null
349
+ );
350
+ }
351
+ if (updates.groupId !== void 0) {
352
+ setClauses.push("group_id = ?");
353
+ params.push(updates.groupId || null);
354
+ }
355
+ if (updates.resolvedAt !== void 0) {
356
+ setClauses.push("resolved_at = ?");
357
+ params.push(updates.resolvedAt || null);
358
+ }
359
+ if (updates.resolvedBy !== void 0) {
360
+ setClauses.push("resolved_by = ?");
361
+ params.push(updates.resolvedBy || null);
362
+ }
363
+ if (updates.resolution !== void 0) {
364
+ setClauses.push("resolution = ?");
365
+ params.push(
366
+ updates.resolution ? JSON.stringify(updates.resolution) : null
367
+ );
368
+ }
369
+ params.push(id);
370
+ this.db.run(
371
+ `UPDATE incidents SET ${setClauses.join(", ")} WHERE id = ?`,
372
+ params
373
+ );
374
+ this.save();
375
+ }
376
+ addIncidentNote(incidentId, note) {
377
+ this.initializeSync();
378
+ const incident = this.getIncident(incidentId);
379
+ if (!incident) return;
380
+ const newNote = {
381
+ id: uuidv4(),
382
+ ...note
383
+ };
384
+ const notes = [...incident.notes, newNote];
385
+ this.db.run(
386
+ "UPDATE incidents SET notes = ?, updated_at = ? WHERE id = ?",
387
+ [JSON.stringify(notes), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
388
+ );
389
+ this.save();
390
+ }
391
+ linkIncidents(incidentId, relatedId) {
392
+ this.initializeSync();
393
+ const incident = this.getIncident(incidentId);
394
+ if (!incident) return;
395
+ if (!incident.relatedIncidents.includes(relatedId)) {
396
+ const related = [...incident.relatedIncidents, relatedId];
397
+ this.db.run(
398
+ "UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
399
+ [JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
400
+ );
401
+ }
402
+ const relatedIncident = this.getIncident(relatedId);
403
+ if (relatedIncident && !relatedIncident.relatedIncidents.includes(incidentId)) {
404
+ const related = [...relatedIncident.relatedIncidents, incidentId];
405
+ this.db.run(
406
+ "UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
407
+ [JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), relatedId]
408
+ );
409
+ }
410
+ this.save();
411
+ }
412
+ getIncidentCount(options = {}) {
413
+ this.initializeSync();
414
+ const conditions = [];
415
+ const params = [];
416
+ if (options.status && options.status !== "all") {
417
+ conditions.push("status = ?");
418
+ params.push(options.status);
419
+ }
420
+ if (options.environment) {
421
+ conditions.push("environment = ?");
422
+ params.push(options.environment);
423
+ }
424
+ if (options.dateFrom) {
425
+ conditions.push("timestamp >= ?");
426
+ params.push(options.dateFrom);
427
+ }
428
+ if (options.dateTo) {
429
+ conditions.push("timestamp <= ?");
430
+ params.push(options.dateTo);
431
+ }
432
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
433
+ const result = this.db.exec(
434
+ `SELECT COUNT(*) as count FROM incidents ${whereClause}`,
435
+ params
436
+ );
437
+ if (result.length === 0 || result[0].values.length === 0) return 0;
438
+ return result[0].values[0][0];
439
+ }
440
+ // ─── Patterns ────────────────────────────────────────────────────
441
+ addPattern(input) {
442
+ this.initializeSync();
443
+ const now = (/* @__PURE__ */ new Date()).toISOString();
444
+ const confidence = {
445
+ ...DEFAULT_CONFIDENCE,
446
+ ...input.confidence
447
+ };
448
+ this.db.run(
449
+ `INSERT INTO patterns (
450
+ id, name, description, pattern, resolution, confidence,
451
+ source, private, tags, created_at, updated_at
452
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
453
+ [
454
+ input.id,
455
+ input.name,
456
+ input.description || null,
457
+ JSON.stringify(input.pattern),
458
+ JSON.stringify(input.resolution),
459
+ JSON.stringify(confidence),
460
+ input.source,
461
+ input.private ? 1 : 0,
462
+ JSON.stringify(input.tags || []),
463
+ now,
464
+ now
465
+ ]
466
+ );
467
+ this.save();
468
+ return input.id;
469
+ }
470
+ getPattern(id) {
471
+ this.initializeSync();
472
+ const result = this.db.exec("SELECT * FROM patterns WHERE id = ?", [id]);
473
+ if (result.length === 0 || result[0].values.length === 0) return null;
474
+ return this.rowToPattern(result[0].columns, result[0].values[0]);
475
+ }
476
+ getAllPatterns(options = {}) {
477
+ this.initializeSync();
478
+ const conditions = [];
479
+ const params = [];
480
+ if (options.source) {
481
+ conditions.push("source = ?");
482
+ params.push(options.source);
483
+ }
484
+ if (options.minConfidence !== void 0) {
485
+ conditions.push("json_extract(confidence, '$.score') >= ?");
486
+ params.push(options.minConfidence);
487
+ }
488
+ if (!options.includePrivate) {
489
+ conditions.push("private = 0");
490
+ }
491
+ if (options.tags && options.tags.length > 0) {
492
+ const tagConditions = options.tags.map(() => "tags LIKE ?");
493
+ conditions.push(`(${tagConditions.join(" OR ")})`);
494
+ params.push(...options.tags.map((tag) => `%"${tag}"%`));
495
+ }
496
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
497
+ const result = this.db.exec(
498
+ `SELECT * FROM patterns ${whereClause} ORDER BY json_extract(confidence, '$.score') DESC`,
499
+ params
500
+ );
501
+ if (result.length === 0) return [];
502
+ return result[0].values.map(
503
+ (row) => this.rowToPattern(result[0].columns, row)
504
+ );
505
+ }
506
+ updatePattern(id, updates) {
507
+ this.initializeSync();
508
+ const now = (/* @__PURE__ */ new Date()).toISOString();
509
+ const setClauses = ["updated_at = ?"];
510
+ const params = [now];
511
+ if (updates.name !== void 0) {
512
+ setClauses.push("name = ?");
513
+ params.push(updates.name);
514
+ }
515
+ if (updates.description !== void 0) {
516
+ setClauses.push("description = ?");
517
+ params.push(updates.description || null);
518
+ }
519
+ if (updates.pattern !== void 0) {
520
+ setClauses.push("pattern = ?");
521
+ params.push(JSON.stringify(updates.pattern));
522
+ }
523
+ if (updates.resolution !== void 0) {
524
+ setClauses.push("resolution = ?");
525
+ params.push(JSON.stringify(updates.resolution));
526
+ }
527
+ if (updates.confidence !== void 0) {
528
+ setClauses.push("confidence = ?");
529
+ params.push(JSON.stringify(updates.confidence));
530
+ }
531
+ if (updates.source !== void 0) {
532
+ setClauses.push("source = ?");
533
+ params.push(updates.source);
534
+ }
535
+ if (updates.private !== void 0) {
536
+ setClauses.push("private = ?");
537
+ params.push(updates.private ? 1 : 0);
538
+ }
539
+ if (updates.tags !== void 0) {
540
+ setClauses.push("tags = ?");
541
+ params.push(JSON.stringify(updates.tags));
542
+ }
543
+ params.push(id);
544
+ this.db.run(
545
+ `UPDATE patterns SET ${setClauses.join(", ")} WHERE id = ?`,
546
+ params
547
+ );
548
+ this.save();
549
+ }
550
+ deletePattern(id) {
551
+ this.initializeSync();
552
+ this.db.run("DELETE FROM patterns WHERE id = ?", [id]);
553
+ this.save();
554
+ }
555
+ updatePatternConfidence(patternId, event) {
556
+ const pattern = this.getPattern(patternId);
557
+ if (!pattern) return;
558
+ const now = (/* @__PURE__ */ new Date()).toISOString();
559
+ const confidence = { ...pattern.confidence };
560
+ switch (event) {
561
+ case "matched":
562
+ confidence.timesMatched++;
563
+ confidence.lastMatched = now;
564
+ break;
565
+ case "resolved":
566
+ confidence.timesResolved++;
567
+ confidence.lastResolved = now;
568
+ confidence.score = Math.min(100, confidence.score + 2);
569
+ break;
570
+ case "recurred":
571
+ confidence.timesRecurred++;
572
+ confidence.score = Math.max(10, confidence.score - 5);
573
+ break;
574
+ }
575
+ this.updatePattern(patternId, { confidence });
576
+ }
577
+ // ─── Groups ──────────────────────────────────────────────────────
578
+ createGroup(input) {
579
+ this.initializeSync();
580
+ const id = `GRP-${uuidv4().substring(0, 8)}`;
581
+ const now = (/* @__PURE__ */ new Date()).toISOString();
582
+ this.db.run(
583
+ `INSERT INTO groups (
584
+ id, name, common_symbols, common_error_patterns,
585
+ suggested_pattern_id, first_seen, last_seen, created_at, updated_at
586
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
587
+ [
588
+ id,
589
+ input.name || null,
590
+ JSON.stringify(input.commonSymbols),
591
+ JSON.stringify(input.commonErrorPatterns),
592
+ input.suggestedPattern?.id || null,
593
+ input.firstSeen,
594
+ input.lastSeen,
595
+ now,
596
+ now
597
+ ]
598
+ );
599
+ for (const incidentId of input.incidents) {
600
+ this.addToGroup(id, incidentId);
601
+ }
602
+ this.save();
603
+ return id;
604
+ }
605
+ getGroup(id) {
606
+ this.initializeSync();
607
+ const result = this.db.exec("SELECT * FROM groups WHERE id = ?", [id]);
608
+ if (result.length === 0 || result[0].values.length === 0) return null;
609
+ return this.rowToGroup(result[0].columns, result[0].values[0]);
610
+ }
611
+ getGroups(options = {}) {
612
+ this.initializeSync();
613
+ const limit = options.limit || 100;
614
+ const result = this.db.exec(
615
+ "SELECT * FROM groups ORDER BY last_seen DESC LIMIT ?",
616
+ [limit]
617
+ );
618
+ if (result.length === 0) return [];
619
+ return result[0].values.map(
620
+ (row) => this.rowToGroup(result[0].columns, row)
621
+ );
622
+ }
623
+ addToGroup(groupId, incidentId) {
624
+ this.initializeSync();
625
+ const now = (/* @__PURE__ */ new Date()).toISOString();
626
+ this.db.run(
627
+ "INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
628
+ [groupId, incidentId, now]
629
+ );
630
+ this.db.run("UPDATE incidents SET group_id = ? WHERE id = ?", [
631
+ groupId,
632
+ incidentId
633
+ ]);
634
+ this.db.run(
635
+ "UPDATE groups SET last_seen = ?, updated_at = ? WHERE id = ?",
636
+ [now, now, groupId]
637
+ );
638
+ this.save();
639
+ }
640
+ // ─── Resolutions ─────────────────────────────────────────────────
641
+ recordResolution(resolution) {
642
+ this.initializeSync();
643
+ const id = uuidv4();
644
+ const now = (/* @__PURE__ */ new Date()).toISOString();
645
+ this.db.run(
646
+ `INSERT INTO resolutions (id, incident_id, pattern_id, commit_hash, pr_url, notes, resolved_at)
647
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
648
+ [
649
+ id,
650
+ resolution.incidentId,
651
+ resolution.patternId || null,
652
+ resolution.commitHash || null,
653
+ resolution.prUrl || null,
654
+ resolution.notes || null,
655
+ now
656
+ ]
657
+ );
658
+ this.updateIncident(resolution.incidentId, {
659
+ status: "resolved",
660
+ resolvedAt: now,
661
+ resolvedBy: resolution.patternId || "manual",
662
+ resolution: {
663
+ patternId: resolution.patternId,
664
+ commitHash: resolution.commitHash,
665
+ prUrl: resolution.prUrl,
666
+ notes: resolution.notes
667
+ }
668
+ });
669
+ if (resolution.patternId) {
670
+ this.updatePatternConfidence(resolution.patternId, "resolved");
671
+ }
672
+ this.save();
673
+ }
674
+ markRecurred(incidentId) {
675
+ this.initializeSync();
676
+ this.db.run(
677
+ "UPDATE resolutions SET recurred = 1 WHERE incident_id = ?",
678
+ [incidentId]
679
+ );
680
+ const result = this.db.exec(
681
+ "SELECT pattern_id FROM resolutions WHERE incident_id = ?",
682
+ [incidentId]
683
+ );
684
+ if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
685
+ this.updatePatternConfidence(result[0].values[0][0], "recurred");
686
+ }
687
+ this.save();
688
+ }
689
+ getResolutionHistory(options = {}) {
690
+ this.initializeSync();
691
+ const conditions = [];
692
+ const params = [];
693
+ if (options.patternId) {
694
+ conditions.push("pattern_id = ?");
695
+ params.push(options.patternId);
696
+ }
697
+ if (options.symbol) {
698
+ conditions.push(`incident_id IN (
699
+ SELECT id FROM incidents WHERE symbols LIKE ?
700
+ )`);
701
+ params.push(`%${options.symbol}%`);
702
+ }
703
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
704
+ const limit = options.limit || 100;
705
+ const result = this.db.exec(
706
+ `SELECT * FROM resolutions ${whereClause} ORDER BY resolved_at DESC LIMIT ?`,
707
+ [...params, limit]
708
+ );
709
+ if (result.length === 0) return [];
710
+ const columns = result[0].columns;
711
+ return result[0].values.map((row) => {
712
+ const obj = {};
713
+ columns.forEach((col, i) => {
714
+ obj[col] = row[i];
715
+ });
716
+ return {
717
+ id: obj.id,
718
+ incidentId: obj.incident_id,
719
+ patternId: obj.pattern_id || void 0,
720
+ commitHash: obj.commit_hash || void 0,
721
+ prUrl: obj.pr_url || void 0,
722
+ notes: obj.notes || void 0,
723
+ resolvedAt: obj.resolved_at,
724
+ recurred: obj.recurred === 1
725
+ };
726
+ });
727
+ }
728
+ // ─── Stats ───────────────────────────────────────────────────────
729
+ getStats(period) {
730
+ this.initializeSync();
731
+ const { start, end } = period;
732
+ const total = this.getIncidentCount({ dateFrom: start, dateTo: end });
733
+ const open = this.getIncidentCount({
734
+ dateFrom: start,
735
+ dateTo: end,
736
+ status: "open"
737
+ });
738
+ const resolved = this.getIncidentCount({
739
+ dateFrom: start,
740
+ dateTo: end,
741
+ status: "resolved"
742
+ });
743
+ const envResult = this.db.exec(
744
+ `SELECT environment, COUNT(*) as count
745
+ FROM incidents
746
+ WHERE timestamp >= ? AND timestamp <= ?
747
+ GROUP BY environment`,
748
+ [start, end]
749
+ );
750
+ const byEnvironment = {};
751
+ if (envResult.length > 0) {
752
+ for (const row of envResult[0].values) {
753
+ byEnvironment[row[0]] = row[1];
754
+ }
755
+ }
756
+ const dayResult = this.db.exec(
757
+ `SELECT DATE(timestamp) as date, COUNT(*) as count
758
+ FROM incidents
759
+ WHERE timestamp >= ? AND timestamp <= ?
760
+ GROUP BY DATE(timestamp)
761
+ ORDER BY date`,
762
+ [start, end]
763
+ );
764
+ const byDay = [];
765
+ if (dayResult.length > 0) {
766
+ for (const row of dayResult[0].values) {
767
+ byDay.push({ date: row[0], count: row[1] });
768
+ }
769
+ }
770
+ const patterns = this.getAllPatterns({ includePrivate: true });
771
+ const avgConfidence = patterns.length > 0 ? patterns.reduce((sum, p) => sum + p.confidence.score, 0) / patterns.length : 0;
772
+ const mostEffective = patterns.sort((a, b) => b.confidence.timesResolved - a.confidence.timesResolved).slice(0, 5).map((p) => ({ patternId: p.id, resolvedCount: p.confidence.timesResolved }));
773
+ const leastEffective = patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
774
+ patternId: p.id,
775
+ recurrenceRate: p.confidence.timesRecurred / Math.max(1, p.confidence.timesResolved)
776
+ })).sort((a, b) => b.recurrenceRate - a.recurrenceRate).slice(0, 5);
777
+ const symbolCounts = /* @__PURE__ */ new Map();
778
+ const incidents = this.getRecentIncidents({
779
+ dateFrom: start,
780
+ dateTo: end,
781
+ limit: 1e3
782
+ });
783
+ for (const incident of incidents) {
784
+ for (const [, value] of Object.entries(incident.symbols)) {
785
+ if (value) {
786
+ symbolCounts.set(value, (symbolCounts.get(value) || 0) + 1);
787
+ }
788
+ }
789
+ }
790
+ const mostIncidents = Array.from(symbolCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([symbol, count]) => ({ symbol, count }));
791
+ const resolutions = this.getResolutionHistory({ limit: 1e3 });
792
+ const periodResolutions = resolutions.filter(
793
+ (r) => r.resolvedAt >= start && r.resolvedAt <= end
794
+ );
795
+ const resolvedWithPattern = periodResolutions.filter(
796
+ (r) => r.patternId
797
+ ).length;
798
+ const resolvedManually = periodResolutions.length - resolvedWithPattern;
799
+ return {
800
+ period: { start, end },
801
+ incidents: {
802
+ total,
803
+ open,
804
+ resolved,
805
+ byEnvironment,
806
+ byDay
807
+ },
808
+ patterns: {
809
+ total: patterns.length,
810
+ avgConfidence: Math.round(avgConfidence),
811
+ mostEffective,
812
+ leastEffective
813
+ },
814
+ symbols: {
815
+ mostIncidents,
816
+ mostResolved: [],
817
+ hotspots: mostIncidents.slice(0, 5).map((s) => ({
818
+ symbol: s.symbol,
819
+ incidentRate: s.count / Math.max(1, total)
820
+ }))
821
+ },
822
+ resolution: {
823
+ avgTimeToResolve: 0,
824
+ resolvedWithPattern,
825
+ resolvedManually,
826
+ resolutionRate: total > 0 ? resolved / total * 100 : 0
827
+ }
828
+ };
829
+ }
830
+ getSymbolHealth(symbol) {
831
+ const incidents = this.getRecentIncidents({ symbol, limit: 1e3 });
832
+ const incidentCount = incidents.length;
833
+ const patternCounts = /* @__PURE__ */ new Map();
834
+ for (const incident of incidents) {
835
+ if (incident.resolution?.patternId) {
836
+ const count = patternCounts.get(incident.resolution.patternId) || 0;
837
+ patternCounts.set(incident.resolution.patternId, count + 1);
838
+ }
839
+ }
840
+ const topPatterns = Array.from(patternCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([patternId, count]) => ({ patternId, count }));
841
+ return {
842
+ incidentCount,
843
+ avgTimeToResolve: 0,
844
+ topPatterns
845
+ };
846
+ }
847
+ // ─── Import/Export ───────────────────────────────────────────────
848
+ exportPatterns(options = {}) {
849
+ const patterns = this.getAllPatterns({
850
+ includePrivate: options.includePrivate
851
+ });
852
+ return {
853
+ version: "1.0.0",
854
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
855
+ patterns
856
+ };
857
+ }
858
+ importPatterns(data, options = {}) {
859
+ let imported = 0;
860
+ let skipped = 0;
861
+ for (const pattern of data.patterns) {
862
+ const existing = this.getPattern(pattern.id);
863
+ if (existing && !options.overwrite) {
864
+ skipped++;
865
+ continue;
866
+ }
867
+ if (existing) {
868
+ this.updatePattern(pattern.id, pattern);
869
+ } else {
870
+ this.addPattern({
871
+ ...pattern,
872
+ source: "imported"
873
+ });
874
+ }
875
+ imported++;
876
+ }
877
+ return { imported, skipped };
878
+ }
879
+ exportBackup() {
880
+ const incidents = this.getRecentIncidents({ limit: 1e5 });
881
+ const patterns = this.getAllPatterns({ includePrivate: true });
882
+ const groups = this.getGroups({ limit: 1e4 });
883
+ return {
884
+ version: "1.0.0",
885
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
886
+ incidents,
887
+ patterns,
888
+ groups
889
+ };
890
+ }
891
+ importBackup(data) {
892
+ this.initializeSync();
893
+ this.db.run("DELETE FROM group_members");
894
+ this.db.run("DELETE FROM resolutions");
895
+ this.db.run("DELETE FROM groups");
896
+ this.db.run("DELETE FROM incidents");
897
+ this.db.run("DELETE FROM patterns");
898
+ for (const pattern of data.patterns) {
899
+ this.addPattern(pattern);
900
+ }
901
+ for (const incident of data.incidents) {
902
+ const now2 = incident.timestamp;
903
+ this.db.run(
904
+ `INSERT INTO incidents (
905
+ id, timestamp, status, error_message, error_stack, error_code, error_type,
906
+ symbols, flow_position, environment, service, version, user_id, request_id,
907
+ group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
908
+ created_at, updated_at
909
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
910
+ [
911
+ incident.id,
912
+ incident.timestamp,
913
+ incident.status,
914
+ incident.error.message,
915
+ incident.error.stack || null,
916
+ incident.error.code || null,
917
+ incident.error.type || null,
918
+ JSON.stringify(incident.symbols),
919
+ incident.flowPosition ? JSON.stringify(incident.flowPosition) : null,
920
+ incident.environment,
921
+ incident.service || null,
922
+ incident.version || null,
923
+ incident.userId || null,
924
+ incident.requestId || null,
925
+ incident.groupId || null,
926
+ JSON.stringify(incident.notes),
927
+ JSON.stringify(incident.relatedIncidents),
928
+ incident.resolvedAt || null,
929
+ incident.resolvedBy || null,
930
+ incident.resolution ? JSON.stringify(incident.resolution) : null,
931
+ now2,
932
+ now2
933
+ ]
934
+ );
935
+ }
936
+ const result = this.db.exec(
937
+ "SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
938
+ );
939
+ if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
940
+ this.incidentCounter = result[0].values[0][0];
941
+ }
942
+ const now = (/* @__PURE__ */ new Date()).toISOString();
943
+ for (const group of data.groups) {
944
+ this.db.run(
945
+ `INSERT INTO groups (
946
+ id, name, common_symbols, common_error_patterns,
947
+ suggested_pattern_id, first_seen, last_seen, created_at, updated_at
948
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
949
+ [
950
+ group.id,
951
+ group.name || null,
952
+ JSON.stringify(group.commonSymbols),
953
+ JSON.stringify(group.commonErrorPatterns),
954
+ group.suggestedPattern?.id || null,
955
+ group.firstSeen,
956
+ group.lastSeen,
957
+ now,
958
+ now
959
+ ]
960
+ );
961
+ for (const incidentId of group.incidents) {
962
+ this.db.run(
963
+ "INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
964
+ [group.id, incidentId, now]
965
+ );
966
+ }
967
+ }
968
+ this.save();
969
+ }
970
+ // ─── Helper Methods ──────────────────────────────────────────────
971
+ rowToIncident(columns, row) {
972
+ const obj = {};
973
+ columns.forEach((col, i) => {
974
+ obj[col] = row[i];
975
+ });
976
+ return {
977
+ id: obj.id,
978
+ timestamp: obj.timestamp,
979
+ status: obj.status,
980
+ error: {
981
+ message: obj.error_message,
982
+ stack: obj.error_stack || void 0,
983
+ code: obj.error_code || void 0,
984
+ type: obj.error_type || void 0
985
+ },
986
+ symbols: JSON.parse(obj.symbols || "{}"),
987
+ flowPosition: obj.flow_position ? JSON.parse(obj.flow_position) : void 0,
988
+ environment: obj.environment,
989
+ service: obj.service || void 0,
990
+ version: obj.version || void 0,
991
+ userId: obj.user_id || void 0,
992
+ requestId: obj.request_id || void 0,
993
+ groupId: obj.group_id || void 0,
994
+ notes: JSON.parse(obj.notes || "[]"),
995
+ relatedIncidents: JSON.parse(obj.related_incidents || "[]"),
996
+ resolvedAt: obj.resolved_at || void 0,
997
+ resolvedBy: obj.resolved_by || void 0,
998
+ resolution: obj.resolution ? JSON.parse(obj.resolution) : void 0
999
+ };
1000
+ }
1001
+ rowToPattern(columns, row) {
1002
+ const obj = {};
1003
+ columns.forEach((col, i) => {
1004
+ obj[col] = row[i];
1005
+ });
1006
+ return {
1007
+ id: obj.id,
1008
+ name: obj.name,
1009
+ description: obj.description || "",
1010
+ pattern: JSON.parse(obj.pattern),
1011
+ resolution: JSON.parse(obj.resolution),
1012
+ confidence: JSON.parse(obj.confidence),
1013
+ source: obj.source,
1014
+ private: obj.private === 1,
1015
+ tags: JSON.parse(obj.tags || "[]"),
1016
+ createdAt: obj.created_at,
1017
+ updatedAt: obj.updated_at
1018
+ };
1019
+ }
1020
+ rowToGroup(columns, row) {
1021
+ const obj = {};
1022
+ columns.forEach((col, i) => {
1023
+ obj[col] = row[i];
1024
+ });
1025
+ const membersResult = this.db.exec(
1026
+ "SELECT incident_id FROM group_members WHERE group_id = ?",
1027
+ [obj.id]
1028
+ );
1029
+ const incidents = [];
1030
+ if (membersResult.length > 0) {
1031
+ for (const r of membersResult[0].values) {
1032
+ incidents.push(r[0]);
1033
+ }
1034
+ }
1035
+ const envResult = this.db.exec(
1036
+ `SELECT DISTINCT environment FROM incidents
1037
+ WHERE id IN (SELECT incident_id FROM group_members WHERE group_id = ?)`,
1038
+ [obj.id]
1039
+ );
1040
+ const environments = [];
1041
+ if (envResult.length > 0) {
1042
+ for (const r of envResult[0].values) {
1043
+ environments.push(r[0]);
1044
+ }
1045
+ }
1046
+ return {
1047
+ id: obj.id,
1048
+ name: obj.name || void 0,
1049
+ incidents,
1050
+ commonSymbols: JSON.parse(obj.common_symbols || "{}"),
1051
+ commonErrorPatterns: JSON.parse(
1052
+ obj.common_error_patterns || "[]"
1053
+ ),
1054
+ count: incidents.length,
1055
+ firstSeen: obj.first_seen,
1056
+ lastSeen: obj.last_seen,
1057
+ environments,
1058
+ suggestedPattern: obj.suggested_pattern_id ? this.getPattern(obj.suggested_pattern_id) || void 0 : void 0
1059
+ };
1060
+ }
1061
+ // ─── Practice Events ─────────────────────────────────────────────
1062
+ recordPracticeEvent(input) {
1063
+ this.initializeSync();
1064
+ const id = `PE-${uuidv4().substring(0, 8)}`;
1065
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1066
+ this.db.run(
1067
+ `INSERT INTO practice_events (
1068
+ id, timestamp, habit_id, habit_category, result,
1069
+ engineer, session_id, lore_entry_id, task_description,
1070
+ symbols_touched, files_modified, related_incident_id, notes
1071
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1072
+ [
1073
+ id,
1074
+ now,
1075
+ input.habitId,
1076
+ input.habitCategory,
1077
+ input.result,
1078
+ input.engineer,
1079
+ input.sessionId,
1080
+ input.loreEntryId || null,
1081
+ input.taskDescription || null,
1082
+ JSON.stringify(input.symbolsTouched || []),
1083
+ JSON.stringify(input.filesModified || []),
1084
+ input.relatedIncidentId || null,
1085
+ input.notes || null
1086
+ ]
1087
+ );
1088
+ this.save();
1089
+ return id;
1090
+ }
1091
+ getPracticeEvents(options = {}) {
1092
+ this.initializeSync();
1093
+ const { limit = 100, offset = 0 } = options;
1094
+ const conditions = [];
1095
+ const params = [];
1096
+ if (options.habitId) {
1097
+ conditions.push("habit_id = ?");
1098
+ params.push(options.habitId);
1099
+ }
1100
+ if (options.habitCategory) {
1101
+ conditions.push("habit_category = ?");
1102
+ params.push(options.habitCategory);
1103
+ }
1104
+ if (options.result) {
1105
+ conditions.push("result = ?");
1106
+ params.push(options.result);
1107
+ }
1108
+ if (options.engineer) {
1109
+ conditions.push("engineer = ?");
1110
+ params.push(options.engineer);
1111
+ }
1112
+ if (options.sessionId) {
1113
+ conditions.push("session_id = ?");
1114
+ params.push(options.sessionId);
1115
+ }
1116
+ if (options.dateFrom) {
1117
+ conditions.push("timestamp >= ?");
1118
+ params.push(options.dateFrom);
1119
+ }
1120
+ if (options.dateTo) {
1121
+ conditions.push("timestamp <= ?");
1122
+ params.push(options.dateTo);
1123
+ }
1124
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1125
+ const result = this.db.exec(
1126
+ `SELECT * FROM practice_events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1127
+ [...params, limit, offset]
1128
+ );
1129
+ if (result.length === 0) return [];
1130
+ return result[0].values.map(
1131
+ (row) => this.rowToPracticeEvent(result[0].columns, row)
1132
+ );
1133
+ }
1134
+ getPracticeEventCount(options = {}) {
1135
+ this.initializeSync();
1136
+ const conditions = [];
1137
+ const params = [];
1138
+ if (options.habitId) {
1139
+ conditions.push("habit_id = ?");
1140
+ params.push(options.habitId);
1141
+ }
1142
+ if (options.habitCategory) {
1143
+ conditions.push("habit_category = ?");
1144
+ params.push(options.habitCategory);
1145
+ }
1146
+ if (options.result) {
1147
+ conditions.push("result = ?");
1148
+ params.push(options.result);
1149
+ }
1150
+ if (options.engineer) {
1151
+ conditions.push("engineer = ?");
1152
+ params.push(options.engineer);
1153
+ }
1154
+ if (options.dateFrom) {
1155
+ conditions.push("timestamp >= ?");
1156
+ params.push(options.dateFrom);
1157
+ }
1158
+ if (options.dateTo) {
1159
+ conditions.push("timestamp <= ?");
1160
+ params.push(options.dateTo);
1161
+ }
1162
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1163
+ const result = this.db.exec(
1164
+ `SELECT COUNT(*) as count FROM practice_events ${whereClause}`,
1165
+ params
1166
+ );
1167
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1168
+ return result[0].values[0][0];
1169
+ }
1170
+ getComplianceRate(options = {}) {
1171
+ this.initializeSync();
1172
+ const conditions = [];
1173
+ const params = [];
1174
+ if (options.habitId) {
1175
+ conditions.push("habit_id = ?");
1176
+ params.push(options.habitId);
1177
+ }
1178
+ if (options.habitCategory) {
1179
+ conditions.push("habit_category = ?");
1180
+ params.push(options.habitCategory);
1181
+ }
1182
+ if (options.engineer) {
1183
+ conditions.push("engineer = ?");
1184
+ params.push(options.engineer);
1185
+ }
1186
+ if (options.dateFrom) {
1187
+ conditions.push("timestamp >= ?");
1188
+ params.push(options.dateFrom);
1189
+ }
1190
+ if (options.dateTo) {
1191
+ conditions.push("timestamp <= ?");
1192
+ params.push(options.dateTo);
1193
+ }
1194
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1195
+ const result = this.db.exec(
1196
+ `SELECT result, COUNT(*) as count
1197
+ FROM practice_events ${whereClause}
1198
+ GROUP BY result`,
1199
+ params
1200
+ );
1201
+ let followed = 0;
1202
+ let skipped = 0;
1203
+ let partial = 0;
1204
+ if (result.length > 0) {
1205
+ for (const row of result[0].values) {
1206
+ const r = row[0];
1207
+ const count = row[1];
1208
+ if (r === "followed") followed = count;
1209
+ else if (r === "skipped") skipped = count;
1210
+ else if (r === "partial") partial = count;
1211
+ }
1212
+ }
1213
+ const total = followed + skipped + partial;
1214
+ const rate = total > 0 ? (followed + partial * 0.5) / total * 100 : 100;
1215
+ return { total, followed, skipped, partial, rate: Math.round(rate) };
1216
+ }
1217
+ rowToPracticeEvent(columns, row) {
1218
+ const obj = {};
1219
+ columns.forEach((col, i) => {
1220
+ obj[col] = row[i];
1221
+ });
1222
+ return {
1223
+ id: obj.id,
1224
+ timestamp: obj.timestamp,
1225
+ habitId: obj.habit_id,
1226
+ habitCategory: obj.habit_category,
1227
+ result: obj.result,
1228
+ engineer: obj.engineer,
1229
+ sessionId: obj.session_id,
1230
+ loreEntryId: obj.lore_entry_id || void 0,
1231
+ taskDescription: obj.task_description || void 0,
1232
+ symbolsTouched: JSON.parse(obj.symbols_touched || "[]"),
1233
+ filesModified: JSON.parse(obj.files_modified || "[]"),
1234
+ relatedIncidentId: obj.related_incident_id || void 0,
1235
+ notes: obj.notes || void 0
1236
+ };
1237
+ }
1238
+ close() {
1239
+ if (this.db) {
1240
+ this.save();
1241
+ this.db.close();
1242
+ this.db = null;
1243
+ }
1244
+ }
1245
+ };
1246
+
1247
+ export {
1248
+ SentinelStorage
1249
+ };