@a-company/sentinel 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/express.d.ts +44 -0
- package/dist/adapters/express.js +30 -0
- package/dist/adapters/fastify.d.ts +23 -0
- package/dist/adapters/fastify.js +18 -0
- package/dist/adapters/hono.d.ts +23 -0
- package/dist/adapters/hono.js +26 -0
- package/dist/chunk-KPMG4XED.js +1249 -0
- package/dist/cli.js +32 -0
- package/dist/commands-KIMGFR2I.js +3278 -0
- package/dist/dist-2F7NO4H4.js +6851 -0
- package/dist/dist-BPWLYV4U.js +6853 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.js +2270 -0
- package/dist/mcp.js +2767 -0
- package/dist/sdk-B27_vK1g.d.ts +644 -0
- package/dist/server/index.d.ts +82 -0
- package/dist/server/index.js +854 -0
- package/package.json +98 -0
- package/src/seeds/loader.ts +45 -0
- package/src/seeds/paradigm-patterns.json +195 -0
- package/src/seeds/universal-patterns.json +292 -0
- package/ui/dist/assets/index-BNgsn_C8.js +62 -0
- package/ui/dist/assets/index-BNgsn_C8.js.map +1 -0
- package/ui/dist/assets/index-DPxatSdT.css +1 -0
- package/ui/dist/index.html +17 -0
- package/ui/dist/sentinel.svg +19 -0
|
@@ -0,0 +1,3278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/commands.ts
|
|
4
|
+
import chalk5 from "chalk";
|
|
5
|
+
|
|
6
|
+
// src/storage.ts
|
|
7
|
+
import initSqlJs from "sql.js";
|
|
8
|
+
import { v4 as uuidv4 } from "uuid";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
var SCHEMA_VERSION = 2;
|
|
12
|
+
var DEFAULT_CONFIDENCE = {
|
|
13
|
+
score: 50,
|
|
14
|
+
timesMatched: 0,
|
|
15
|
+
timesResolved: 0,
|
|
16
|
+
timesRecurred: 0
|
|
17
|
+
};
|
|
18
|
+
var SQL = null;
|
|
19
|
+
var SentinelStorage = class {
|
|
20
|
+
db = null;
|
|
21
|
+
dbPath;
|
|
22
|
+
incidentCounter = 0;
|
|
23
|
+
initialized = false;
|
|
24
|
+
constructor(dbPath) {
|
|
25
|
+
this.dbPath = dbPath || this.getDefaultDbPath();
|
|
26
|
+
}
|
|
27
|
+
getDefaultDbPath() {
|
|
28
|
+
const dataDir = process.env.SENTINEL_DATA_DIR || process.env.PARADIGM_DATA_DIR || path.join(process.cwd(), ".paradigm", "sentinel");
|
|
29
|
+
return path.join(dataDir, "sentinel.db");
|
|
30
|
+
}
|
|
31
|
+
createSchema() {
|
|
32
|
+
if (!this.db) return;
|
|
33
|
+
this.db.run(`
|
|
34
|
+
-- Metadata table for schema versioning
|
|
35
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
36
|
+
key TEXT PRIMARY KEY,
|
|
37
|
+
value TEXT NOT NULL
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- Incidents table
|
|
41
|
+
CREATE TABLE IF NOT EXISTS incidents (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
timestamp TEXT NOT NULL,
|
|
44
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
45
|
+
error_message TEXT NOT NULL,
|
|
46
|
+
error_stack TEXT,
|
|
47
|
+
error_code TEXT,
|
|
48
|
+
error_type TEXT,
|
|
49
|
+
symbols TEXT NOT NULL,
|
|
50
|
+
flow_position TEXT,
|
|
51
|
+
environment TEXT NOT NULL,
|
|
52
|
+
service TEXT,
|
|
53
|
+
version TEXT,
|
|
54
|
+
user_id TEXT,
|
|
55
|
+
request_id TEXT,
|
|
56
|
+
group_id TEXT,
|
|
57
|
+
notes TEXT DEFAULT '[]',
|
|
58
|
+
related_incidents TEXT DEFAULT '[]',
|
|
59
|
+
resolved_at TEXT,
|
|
60
|
+
resolved_by TEXT,
|
|
61
|
+
resolution TEXT,
|
|
62
|
+
created_at TEXT NOT NULL,
|
|
63
|
+
updated_at TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Patterns table
|
|
67
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
name TEXT NOT NULL,
|
|
70
|
+
description TEXT,
|
|
71
|
+
pattern TEXT NOT NULL,
|
|
72
|
+
resolution TEXT NOT NULL,
|
|
73
|
+
confidence TEXT NOT NULL,
|
|
74
|
+
source TEXT NOT NULL DEFAULT 'manual',
|
|
75
|
+
private INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
tags TEXT DEFAULT '[]',
|
|
77
|
+
created_at TEXT NOT NULL,
|
|
78
|
+
updated_at TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
-- Incident groups
|
|
82
|
+
CREATE TABLE IF NOT EXISTS groups (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
name TEXT,
|
|
85
|
+
common_symbols TEXT,
|
|
86
|
+
common_error_patterns TEXT,
|
|
87
|
+
suggested_pattern_id TEXT,
|
|
88
|
+
first_seen TEXT NOT NULL,
|
|
89
|
+
last_seen TEXT NOT NULL,
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
updated_at TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
-- Group members
|
|
95
|
+
CREATE TABLE IF NOT EXISTS group_members (
|
|
96
|
+
group_id TEXT NOT NULL,
|
|
97
|
+
incident_id TEXT NOT NULL,
|
|
98
|
+
added_at TEXT NOT NULL,
|
|
99
|
+
PRIMARY KEY (group_id, incident_id)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
-- Resolutions history
|
|
103
|
+
CREATE TABLE IF NOT EXISTS resolutions (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
incident_id TEXT NOT NULL,
|
|
106
|
+
pattern_id TEXT,
|
|
107
|
+
commit_hash TEXT,
|
|
108
|
+
pr_url TEXT,
|
|
109
|
+
notes TEXT,
|
|
110
|
+
resolved_at TEXT NOT NULL,
|
|
111
|
+
recurred INTEGER DEFAULT 0
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
-- Practice events (habits system)
|
|
115
|
+
CREATE TABLE IF NOT EXISTS practice_events (
|
|
116
|
+
id TEXT PRIMARY KEY,
|
|
117
|
+
timestamp TEXT NOT NULL,
|
|
118
|
+
habit_id TEXT NOT NULL,
|
|
119
|
+
habit_category TEXT NOT NULL,
|
|
120
|
+
result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
|
|
121
|
+
engineer TEXT NOT NULL,
|
|
122
|
+
session_id TEXT NOT NULL,
|
|
123
|
+
lore_entry_id TEXT,
|
|
124
|
+
task_description TEXT,
|
|
125
|
+
symbols_touched TEXT DEFAULT '[]',
|
|
126
|
+
files_modified TEXT DEFAULT '[]',
|
|
127
|
+
related_incident_id TEXT,
|
|
128
|
+
notes TEXT
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
-- Indexes
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_environment ON incidents(environment);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_source ON patterns(source);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
|
|
140
|
+
`);
|
|
141
|
+
this.db.run(
|
|
142
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
|
|
143
|
+
[String(SCHEMA_VERSION)]
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
save() {
|
|
147
|
+
if (!this.db) return;
|
|
148
|
+
const data = this.db.export();
|
|
149
|
+
const buffer = Buffer.from(data);
|
|
150
|
+
fs.writeFileSync(this.dbPath, buffer);
|
|
151
|
+
}
|
|
152
|
+
// ─── Incidents ───────────────────────────────────────────────────
|
|
153
|
+
recordIncident(input) {
|
|
154
|
+
const db = this.db;
|
|
155
|
+
if (!db) {
|
|
156
|
+
this.initializeSync();
|
|
157
|
+
}
|
|
158
|
+
this.incidentCounter++;
|
|
159
|
+
const id = `INC-${String(this.incidentCounter).padStart(3, "0")}`;
|
|
160
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
161
|
+
this.db.run(
|
|
162
|
+
`INSERT INTO incidents (
|
|
163
|
+
id, timestamp, status, error_message, error_stack, error_code, error_type,
|
|
164
|
+
symbols, flow_position, environment, service, version, user_id, request_id,
|
|
165
|
+
group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
|
|
166
|
+
created_at, updated_at
|
|
167
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
168
|
+
[
|
|
169
|
+
id,
|
|
170
|
+
input.timestamp || now,
|
|
171
|
+
input.status || "open",
|
|
172
|
+
input.error.message,
|
|
173
|
+
input.error.stack || null,
|
|
174
|
+
input.error.code || null,
|
|
175
|
+
input.error.type || null,
|
|
176
|
+
JSON.stringify(input.symbols),
|
|
177
|
+
input.flowPosition ? JSON.stringify(input.flowPosition) : null,
|
|
178
|
+
input.environment,
|
|
179
|
+
input.service || null,
|
|
180
|
+
input.version || null,
|
|
181
|
+
input.userId || null,
|
|
182
|
+
input.requestId || null,
|
|
183
|
+
input.groupId || null,
|
|
184
|
+
"[]",
|
|
185
|
+
"[]",
|
|
186
|
+
input.resolvedAt || null,
|
|
187
|
+
input.resolvedBy || null,
|
|
188
|
+
input.resolution ? JSON.stringify(input.resolution) : null,
|
|
189
|
+
now,
|
|
190
|
+
now
|
|
191
|
+
]
|
|
192
|
+
);
|
|
193
|
+
this.save();
|
|
194
|
+
return id;
|
|
195
|
+
}
|
|
196
|
+
initializeSync() {
|
|
197
|
+
if (this.initialized && this.db) return;
|
|
198
|
+
if (!this.db && SQL) {
|
|
199
|
+
const dir = path.dirname(this.dbPath);
|
|
200
|
+
if (!fs.existsSync(dir)) {
|
|
201
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
if (fs.existsSync(this.dbPath)) {
|
|
204
|
+
const fileData = fs.readFileSync(this.dbPath);
|
|
205
|
+
this.db = new SQL.Database(fileData);
|
|
206
|
+
} else {
|
|
207
|
+
this.db = new SQL.Database();
|
|
208
|
+
this.createSchema();
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const result = this.db.exec(
|
|
212
|
+
"SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
|
|
213
|
+
);
|
|
214
|
+
if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
|
|
215
|
+
this.incidentCounter = result[0].values[0][0];
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
this.incidentCounter = 0;
|
|
219
|
+
}
|
|
220
|
+
this.migrateSchema();
|
|
221
|
+
this.initialized = true;
|
|
222
|
+
this.save();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Run schema migrations from older versions
|
|
227
|
+
*/
|
|
228
|
+
migrateSchema() {
|
|
229
|
+
if (!this.db) return;
|
|
230
|
+
let currentVersion = 1;
|
|
231
|
+
try {
|
|
232
|
+
const result = this.db.exec(
|
|
233
|
+
"SELECT value FROM metadata WHERE key = 'schema_version'"
|
|
234
|
+
);
|
|
235
|
+
if (result.length > 0 && result[0].values.length > 0) {
|
|
236
|
+
currentVersion = parseInt(result[0].values[0][0], 10) || 1;
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
if (currentVersion < 2) {
|
|
241
|
+
try {
|
|
242
|
+
this.db.run(`
|
|
243
|
+
CREATE TABLE IF NOT EXISTS practice_events (
|
|
244
|
+
id TEXT PRIMARY KEY,
|
|
245
|
+
timestamp TEXT NOT NULL,
|
|
246
|
+
habit_id TEXT NOT NULL,
|
|
247
|
+
habit_category TEXT NOT NULL,
|
|
248
|
+
result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
|
|
249
|
+
engineer TEXT NOT NULL,
|
|
250
|
+
session_id TEXT NOT NULL,
|
|
251
|
+
lore_entry_id TEXT,
|
|
252
|
+
task_description TEXT,
|
|
253
|
+
symbols_touched TEXT DEFAULT '[]',
|
|
254
|
+
files_modified TEXT DEFAULT '[]',
|
|
255
|
+
related_incident_id TEXT,
|
|
256
|
+
notes TEXT
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
|
|
263
|
+
`);
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
this.db.run(
|
|
267
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Ensure the storage is ready for use. Must be called once before using storage methods.
|
|
273
|
+
*/
|
|
274
|
+
async ensureReady() {
|
|
275
|
+
if (!SQL) {
|
|
276
|
+
SQL = await initSqlJs();
|
|
277
|
+
}
|
|
278
|
+
this.initializeSync();
|
|
279
|
+
}
|
|
280
|
+
getIncident(id) {
|
|
281
|
+
this.initializeSync();
|
|
282
|
+
const result = this.db.exec("SELECT * FROM incidents WHERE id = ?", [id]);
|
|
283
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
284
|
+
return this.rowToIncident(result[0].columns, result[0].values[0]);
|
|
285
|
+
}
|
|
286
|
+
getRecentIncidents(options = {}) {
|
|
287
|
+
this.initializeSync();
|
|
288
|
+
const { limit = 50, offset = 0 } = options;
|
|
289
|
+
const conditions = [];
|
|
290
|
+
const params = [];
|
|
291
|
+
if (options.status && options.status !== "all") {
|
|
292
|
+
conditions.push("status = ?");
|
|
293
|
+
params.push(options.status);
|
|
294
|
+
}
|
|
295
|
+
if (options.environment) {
|
|
296
|
+
conditions.push("environment = ?");
|
|
297
|
+
params.push(options.environment);
|
|
298
|
+
}
|
|
299
|
+
if (options.symbol) {
|
|
300
|
+
conditions.push("symbols LIKE ?");
|
|
301
|
+
params.push(`%${options.symbol}%`);
|
|
302
|
+
}
|
|
303
|
+
if (options.search) {
|
|
304
|
+
conditions.push("(error_message LIKE ? OR notes LIKE ?)");
|
|
305
|
+
params.push(`%${options.search}%`, `%${options.search}%`);
|
|
306
|
+
}
|
|
307
|
+
if (options.dateFrom) {
|
|
308
|
+
conditions.push("timestamp >= ?");
|
|
309
|
+
params.push(options.dateFrom);
|
|
310
|
+
}
|
|
311
|
+
if (options.dateTo) {
|
|
312
|
+
conditions.push("timestamp <= ?");
|
|
313
|
+
params.push(options.dateTo);
|
|
314
|
+
}
|
|
315
|
+
if (options.groupId) {
|
|
316
|
+
conditions.push("group_id = ?");
|
|
317
|
+
params.push(options.groupId);
|
|
318
|
+
}
|
|
319
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
320
|
+
const result = this.db.exec(
|
|
321
|
+
`SELECT * FROM incidents ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
322
|
+
[...params, limit, offset]
|
|
323
|
+
);
|
|
324
|
+
if (result.length === 0) return [];
|
|
325
|
+
return result[0].values.map(
|
|
326
|
+
(row) => this.rowToIncident(result[0].columns, row)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
updateIncident(id, updates) {
|
|
330
|
+
this.initializeSync();
|
|
331
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
332
|
+
const setClauses = ["updated_at = ?"];
|
|
333
|
+
const params = [now];
|
|
334
|
+
if (updates.status !== void 0) {
|
|
335
|
+
setClauses.push("status = ?");
|
|
336
|
+
params.push(updates.status);
|
|
337
|
+
}
|
|
338
|
+
if (updates.error !== void 0) {
|
|
339
|
+
setClauses.push("error_message = ?");
|
|
340
|
+
params.push(updates.error.message);
|
|
341
|
+
if (updates.error.stack !== void 0) {
|
|
342
|
+
setClauses.push("error_stack = ?");
|
|
343
|
+
params.push(updates.error.stack || null);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (updates.symbols !== void 0) {
|
|
347
|
+
setClauses.push("symbols = ?");
|
|
348
|
+
params.push(JSON.stringify(updates.symbols));
|
|
349
|
+
}
|
|
350
|
+
if (updates.flowPosition !== void 0) {
|
|
351
|
+
setClauses.push("flow_position = ?");
|
|
352
|
+
params.push(
|
|
353
|
+
updates.flowPosition ? JSON.stringify(updates.flowPosition) : null
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (updates.groupId !== void 0) {
|
|
357
|
+
setClauses.push("group_id = ?");
|
|
358
|
+
params.push(updates.groupId || null);
|
|
359
|
+
}
|
|
360
|
+
if (updates.resolvedAt !== void 0) {
|
|
361
|
+
setClauses.push("resolved_at = ?");
|
|
362
|
+
params.push(updates.resolvedAt || null);
|
|
363
|
+
}
|
|
364
|
+
if (updates.resolvedBy !== void 0) {
|
|
365
|
+
setClauses.push("resolved_by = ?");
|
|
366
|
+
params.push(updates.resolvedBy || null);
|
|
367
|
+
}
|
|
368
|
+
if (updates.resolution !== void 0) {
|
|
369
|
+
setClauses.push("resolution = ?");
|
|
370
|
+
params.push(
|
|
371
|
+
updates.resolution ? JSON.stringify(updates.resolution) : null
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
params.push(id);
|
|
375
|
+
this.db.run(
|
|
376
|
+
`UPDATE incidents SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
377
|
+
params
|
|
378
|
+
);
|
|
379
|
+
this.save();
|
|
380
|
+
}
|
|
381
|
+
addIncidentNote(incidentId, note) {
|
|
382
|
+
this.initializeSync();
|
|
383
|
+
const incident = this.getIncident(incidentId);
|
|
384
|
+
if (!incident) return;
|
|
385
|
+
const newNote = {
|
|
386
|
+
id: uuidv4(),
|
|
387
|
+
...note
|
|
388
|
+
};
|
|
389
|
+
const notes = [...incident.notes, newNote];
|
|
390
|
+
this.db.run(
|
|
391
|
+
"UPDATE incidents SET notes = ?, updated_at = ? WHERE id = ?",
|
|
392
|
+
[JSON.stringify(notes), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
|
|
393
|
+
);
|
|
394
|
+
this.save();
|
|
395
|
+
}
|
|
396
|
+
linkIncidents(incidentId, relatedId) {
|
|
397
|
+
this.initializeSync();
|
|
398
|
+
const incident = this.getIncident(incidentId);
|
|
399
|
+
if (!incident) return;
|
|
400
|
+
if (!incident.relatedIncidents.includes(relatedId)) {
|
|
401
|
+
const related = [...incident.relatedIncidents, relatedId];
|
|
402
|
+
this.db.run(
|
|
403
|
+
"UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
|
|
404
|
+
[JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const relatedIncident = this.getIncident(relatedId);
|
|
408
|
+
if (relatedIncident && !relatedIncident.relatedIncidents.includes(incidentId)) {
|
|
409
|
+
const related = [...relatedIncident.relatedIncidents, incidentId];
|
|
410
|
+
this.db.run(
|
|
411
|
+
"UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
|
|
412
|
+
[JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), relatedId]
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
this.save();
|
|
416
|
+
}
|
|
417
|
+
getIncidentCount(options = {}) {
|
|
418
|
+
this.initializeSync();
|
|
419
|
+
const conditions = [];
|
|
420
|
+
const params = [];
|
|
421
|
+
if (options.status && options.status !== "all") {
|
|
422
|
+
conditions.push("status = ?");
|
|
423
|
+
params.push(options.status);
|
|
424
|
+
}
|
|
425
|
+
if (options.environment) {
|
|
426
|
+
conditions.push("environment = ?");
|
|
427
|
+
params.push(options.environment);
|
|
428
|
+
}
|
|
429
|
+
if (options.dateFrom) {
|
|
430
|
+
conditions.push("timestamp >= ?");
|
|
431
|
+
params.push(options.dateFrom);
|
|
432
|
+
}
|
|
433
|
+
if (options.dateTo) {
|
|
434
|
+
conditions.push("timestamp <= ?");
|
|
435
|
+
params.push(options.dateTo);
|
|
436
|
+
}
|
|
437
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
438
|
+
const result = this.db.exec(
|
|
439
|
+
`SELECT COUNT(*) as count FROM incidents ${whereClause}`,
|
|
440
|
+
params
|
|
441
|
+
);
|
|
442
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
443
|
+
return result[0].values[0][0];
|
|
444
|
+
}
|
|
445
|
+
// ─── Patterns ────────────────────────────────────────────────────
|
|
446
|
+
addPattern(input) {
|
|
447
|
+
this.initializeSync();
|
|
448
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
449
|
+
const confidence = {
|
|
450
|
+
...DEFAULT_CONFIDENCE,
|
|
451
|
+
...input.confidence
|
|
452
|
+
};
|
|
453
|
+
this.db.run(
|
|
454
|
+
`INSERT INTO patterns (
|
|
455
|
+
id, name, description, pattern, resolution, confidence,
|
|
456
|
+
source, private, tags, created_at, updated_at
|
|
457
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
458
|
+
[
|
|
459
|
+
input.id,
|
|
460
|
+
input.name,
|
|
461
|
+
input.description || null,
|
|
462
|
+
JSON.stringify(input.pattern),
|
|
463
|
+
JSON.stringify(input.resolution),
|
|
464
|
+
JSON.stringify(confidence),
|
|
465
|
+
input.source,
|
|
466
|
+
input.private ? 1 : 0,
|
|
467
|
+
JSON.stringify(input.tags || []),
|
|
468
|
+
now,
|
|
469
|
+
now
|
|
470
|
+
]
|
|
471
|
+
);
|
|
472
|
+
this.save();
|
|
473
|
+
return input.id;
|
|
474
|
+
}
|
|
475
|
+
getPattern(id) {
|
|
476
|
+
this.initializeSync();
|
|
477
|
+
const result = this.db.exec("SELECT * FROM patterns WHERE id = ?", [id]);
|
|
478
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
479
|
+
return this.rowToPattern(result[0].columns, result[0].values[0]);
|
|
480
|
+
}
|
|
481
|
+
getAllPatterns(options = {}) {
|
|
482
|
+
this.initializeSync();
|
|
483
|
+
const conditions = [];
|
|
484
|
+
const params = [];
|
|
485
|
+
if (options.source) {
|
|
486
|
+
conditions.push("source = ?");
|
|
487
|
+
params.push(options.source);
|
|
488
|
+
}
|
|
489
|
+
if (options.minConfidence !== void 0) {
|
|
490
|
+
conditions.push("json_extract(confidence, '$.score') >= ?");
|
|
491
|
+
params.push(options.minConfidence);
|
|
492
|
+
}
|
|
493
|
+
if (!options.includePrivate) {
|
|
494
|
+
conditions.push("private = 0");
|
|
495
|
+
}
|
|
496
|
+
if (options.tags && options.tags.length > 0) {
|
|
497
|
+
const tagConditions = options.tags.map(() => "tags LIKE ?");
|
|
498
|
+
conditions.push(`(${tagConditions.join(" OR ")})`);
|
|
499
|
+
params.push(...options.tags.map((tag) => `%"${tag}"%`));
|
|
500
|
+
}
|
|
501
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
502
|
+
const result = this.db.exec(
|
|
503
|
+
`SELECT * FROM patterns ${whereClause} ORDER BY json_extract(confidence, '$.score') DESC`,
|
|
504
|
+
params
|
|
505
|
+
);
|
|
506
|
+
if (result.length === 0) return [];
|
|
507
|
+
return result[0].values.map(
|
|
508
|
+
(row) => this.rowToPattern(result[0].columns, row)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
updatePattern(id, updates) {
|
|
512
|
+
this.initializeSync();
|
|
513
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
514
|
+
const setClauses = ["updated_at = ?"];
|
|
515
|
+
const params = [now];
|
|
516
|
+
if (updates.name !== void 0) {
|
|
517
|
+
setClauses.push("name = ?");
|
|
518
|
+
params.push(updates.name);
|
|
519
|
+
}
|
|
520
|
+
if (updates.description !== void 0) {
|
|
521
|
+
setClauses.push("description = ?");
|
|
522
|
+
params.push(updates.description || null);
|
|
523
|
+
}
|
|
524
|
+
if (updates.pattern !== void 0) {
|
|
525
|
+
setClauses.push("pattern = ?");
|
|
526
|
+
params.push(JSON.stringify(updates.pattern));
|
|
527
|
+
}
|
|
528
|
+
if (updates.resolution !== void 0) {
|
|
529
|
+
setClauses.push("resolution = ?");
|
|
530
|
+
params.push(JSON.stringify(updates.resolution));
|
|
531
|
+
}
|
|
532
|
+
if (updates.confidence !== void 0) {
|
|
533
|
+
setClauses.push("confidence = ?");
|
|
534
|
+
params.push(JSON.stringify(updates.confidence));
|
|
535
|
+
}
|
|
536
|
+
if (updates.source !== void 0) {
|
|
537
|
+
setClauses.push("source = ?");
|
|
538
|
+
params.push(updates.source);
|
|
539
|
+
}
|
|
540
|
+
if (updates.private !== void 0) {
|
|
541
|
+
setClauses.push("private = ?");
|
|
542
|
+
params.push(updates.private ? 1 : 0);
|
|
543
|
+
}
|
|
544
|
+
if (updates.tags !== void 0) {
|
|
545
|
+
setClauses.push("tags = ?");
|
|
546
|
+
params.push(JSON.stringify(updates.tags));
|
|
547
|
+
}
|
|
548
|
+
params.push(id);
|
|
549
|
+
this.db.run(
|
|
550
|
+
`UPDATE patterns SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
551
|
+
params
|
|
552
|
+
);
|
|
553
|
+
this.save();
|
|
554
|
+
}
|
|
555
|
+
deletePattern(id) {
|
|
556
|
+
this.initializeSync();
|
|
557
|
+
this.db.run("DELETE FROM patterns WHERE id = ?", [id]);
|
|
558
|
+
this.save();
|
|
559
|
+
}
|
|
560
|
+
updatePatternConfidence(patternId, event) {
|
|
561
|
+
const pattern = this.getPattern(patternId);
|
|
562
|
+
if (!pattern) return;
|
|
563
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
564
|
+
const confidence = { ...pattern.confidence };
|
|
565
|
+
switch (event) {
|
|
566
|
+
case "matched":
|
|
567
|
+
confidence.timesMatched++;
|
|
568
|
+
confidence.lastMatched = now;
|
|
569
|
+
break;
|
|
570
|
+
case "resolved":
|
|
571
|
+
confidence.timesResolved++;
|
|
572
|
+
confidence.lastResolved = now;
|
|
573
|
+
confidence.score = Math.min(100, confidence.score + 2);
|
|
574
|
+
break;
|
|
575
|
+
case "recurred":
|
|
576
|
+
confidence.timesRecurred++;
|
|
577
|
+
confidence.score = Math.max(10, confidence.score - 5);
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
this.updatePattern(patternId, { confidence });
|
|
581
|
+
}
|
|
582
|
+
// ─── Groups ──────────────────────────────────────────────────────
|
|
583
|
+
createGroup(input) {
|
|
584
|
+
this.initializeSync();
|
|
585
|
+
const id = `GRP-${uuidv4().substring(0, 8)}`;
|
|
586
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
587
|
+
this.db.run(
|
|
588
|
+
`INSERT INTO groups (
|
|
589
|
+
id, name, common_symbols, common_error_patterns,
|
|
590
|
+
suggested_pattern_id, first_seen, last_seen, created_at, updated_at
|
|
591
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
592
|
+
[
|
|
593
|
+
id,
|
|
594
|
+
input.name || null,
|
|
595
|
+
JSON.stringify(input.commonSymbols),
|
|
596
|
+
JSON.stringify(input.commonErrorPatterns),
|
|
597
|
+
input.suggestedPattern?.id || null,
|
|
598
|
+
input.firstSeen,
|
|
599
|
+
input.lastSeen,
|
|
600
|
+
now,
|
|
601
|
+
now
|
|
602
|
+
]
|
|
603
|
+
);
|
|
604
|
+
for (const incidentId of input.incidents) {
|
|
605
|
+
this.addToGroup(id, incidentId);
|
|
606
|
+
}
|
|
607
|
+
this.save();
|
|
608
|
+
return id;
|
|
609
|
+
}
|
|
610
|
+
getGroup(id) {
|
|
611
|
+
this.initializeSync();
|
|
612
|
+
const result = this.db.exec("SELECT * FROM groups WHERE id = ?", [id]);
|
|
613
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
614
|
+
return this.rowToGroup(result[0].columns, result[0].values[0]);
|
|
615
|
+
}
|
|
616
|
+
getGroups(options = {}) {
|
|
617
|
+
this.initializeSync();
|
|
618
|
+
const limit = options.limit || 100;
|
|
619
|
+
const result = this.db.exec(
|
|
620
|
+
"SELECT * FROM groups ORDER BY last_seen DESC LIMIT ?",
|
|
621
|
+
[limit]
|
|
622
|
+
);
|
|
623
|
+
if (result.length === 0) return [];
|
|
624
|
+
return result[0].values.map(
|
|
625
|
+
(row) => this.rowToGroup(result[0].columns, row)
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
addToGroup(groupId, incidentId) {
|
|
629
|
+
this.initializeSync();
|
|
630
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
631
|
+
this.db.run(
|
|
632
|
+
"INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
|
|
633
|
+
[groupId, incidentId, now]
|
|
634
|
+
);
|
|
635
|
+
this.db.run("UPDATE incidents SET group_id = ? WHERE id = ?", [
|
|
636
|
+
groupId,
|
|
637
|
+
incidentId
|
|
638
|
+
]);
|
|
639
|
+
this.db.run(
|
|
640
|
+
"UPDATE groups SET last_seen = ?, updated_at = ? WHERE id = ?",
|
|
641
|
+
[now, now, groupId]
|
|
642
|
+
);
|
|
643
|
+
this.save();
|
|
644
|
+
}
|
|
645
|
+
// ─── Resolutions ─────────────────────────────────────────────────
|
|
646
|
+
recordResolution(resolution) {
|
|
647
|
+
this.initializeSync();
|
|
648
|
+
const id = uuidv4();
|
|
649
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
650
|
+
this.db.run(
|
|
651
|
+
`INSERT INTO resolutions (id, incident_id, pattern_id, commit_hash, pr_url, notes, resolved_at)
|
|
652
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
653
|
+
[
|
|
654
|
+
id,
|
|
655
|
+
resolution.incidentId,
|
|
656
|
+
resolution.patternId || null,
|
|
657
|
+
resolution.commitHash || null,
|
|
658
|
+
resolution.prUrl || null,
|
|
659
|
+
resolution.notes || null,
|
|
660
|
+
now
|
|
661
|
+
]
|
|
662
|
+
);
|
|
663
|
+
this.updateIncident(resolution.incidentId, {
|
|
664
|
+
status: "resolved",
|
|
665
|
+
resolvedAt: now,
|
|
666
|
+
resolvedBy: resolution.patternId || "manual",
|
|
667
|
+
resolution: {
|
|
668
|
+
patternId: resolution.patternId,
|
|
669
|
+
commitHash: resolution.commitHash,
|
|
670
|
+
prUrl: resolution.prUrl,
|
|
671
|
+
notes: resolution.notes
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
if (resolution.patternId) {
|
|
675
|
+
this.updatePatternConfidence(resolution.patternId, "resolved");
|
|
676
|
+
}
|
|
677
|
+
this.save();
|
|
678
|
+
}
|
|
679
|
+
markRecurred(incidentId) {
|
|
680
|
+
this.initializeSync();
|
|
681
|
+
this.db.run(
|
|
682
|
+
"UPDATE resolutions SET recurred = 1 WHERE incident_id = ?",
|
|
683
|
+
[incidentId]
|
|
684
|
+
);
|
|
685
|
+
const result = this.db.exec(
|
|
686
|
+
"SELECT pattern_id FROM resolutions WHERE incident_id = ?",
|
|
687
|
+
[incidentId]
|
|
688
|
+
);
|
|
689
|
+
if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
|
|
690
|
+
this.updatePatternConfidence(result[0].values[0][0], "recurred");
|
|
691
|
+
}
|
|
692
|
+
this.save();
|
|
693
|
+
}
|
|
694
|
+
getResolutionHistory(options = {}) {
|
|
695
|
+
this.initializeSync();
|
|
696
|
+
const conditions = [];
|
|
697
|
+
const params = [];
|
|
698
|
+
if (options.patternId) {
|
|
699
|
+
conditions.push("pattern_id = ?");
|
|
700
|
+
params.push(options.patternId);
|
|
701
|
+
}
|
|
702
|
+
if (options.symbol) {
|
|
703
|
+
conditions.push(`incident_id IN (
|
|
704
|
+
SELECT id FROM incidents WHERE symbols LIKE ?
|
|
705
|
+
)`);
|
|
706
|
+
params.push(`%${options.symbol}%`);
|
|
707
|
+
}
|
|
708
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
709
|
+
const limit = options.limit || 100;
|
|
710
|
+
const result = this.db.exec(
|
|
711
|
+
`SELECT * FROM resolutions ${whereClause} ORDER BY resolved_at DESC LIMIT ?`,
|
|
712
|
+
[...params, limit]
|
|
713
|
+
);
|
|
714
|
+
if (result.length === 0) return [];
|
|
715
|
+
const columns = result[0].columns;
|
|
716
|
+
return result[0].values.map((row) => {
|
|
717
|
+
const obj = {};
|
|
718
|
+
columns.forEach((col, i) => {
|
|
719
|
+
obj[col] = row[i];
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
id: obj.id,
|
|
723
|
+
incidentId: obj.incident_id,
|
|
724
|
+
patternId: obj.pattern_id || void 0,
|
|
725
|
+
commitHash: obj.commit_hash || void 0,
|
|
726
|
+
prUrl: obj.pr_url || void 0,
|
|
727
|
+
notes: obj.notes || void 0,
|
|
728
|
+
resolvedAt: obj.resolved_at,
|
|
729
|
+
recurred: obj.recurred === 1
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
// ─── Stats ───────────────────────────────────────────────────────
|
|
734
|
+
getStats(period) {
|
|
735
|
+
this.initializeSync();
|
|
736
|
+
const { start, end } = period;
|
|
737
|
+
const total = this.getIncidentCount({ dateFrom: start, dateTo: end });
|
|
738
|
+
const open = this.getIncidentCount({
|
|
739
|
+
dateFrom: start,
|
|
740
|
+
dateTo: end,
|
|
741
|
+
status: "open"
|
|
742
|
+
});
|
|
743
|
+
const resolved = this.getIncidentCount({
|
|
744
|
+
dateFrom: start,
|
|
745
|
+
dateTo: end,
|
|
746
|
+
status: "resolved"
|
|
747
|
+
});
|
|
748
|
+
const envResult = this.db.exec(
|
|
749
|
+
`SELECT environment, COUNT(*) as count
|
|
750
|
+
FROM incidents
|
|
751
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
752
|
+
GROUP BY environment`,
|
|
753
|
+
[start, end]
|
|
754
|
+
);
|
|
755
|
+
const byEnvironment = {};
|
|
756
|
+
if (envResult.length > 0) {
|
|
757
|
+
for (const row of envResult[0].values) {
|
|
758
|
+
byEnvironment[row[0]] = row[1];
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const dayResult = this.db.exec(
|
|
762
|
+
`SELECT DATE(timestamp) as date, COUNT(*) as count
|
|
763
|
+
FROM incidents
|
|
764
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
765
|
+
GROUP BY DATE(timestamp)
|
|
766
|
+
ORDER BY date`,
|
|
767
|
+
[start, end]
|
|
768
|
+
);
|
|
769
|
+
const byDay = [];
|
|
770
|
+
if (dayResult.length > 0) {
|
|
771
|
+
for (const row of dayResult[0].values) {
|
|
772
|
+
byDay.push({ date: row[0], count: row[1] });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const patterns = this.getAllPatterns({ includePrivate: true });
|
|
776
|
+
const avgConfidence = patterns.length > 0 ? patterns.reduce((sum, p) => sum + p.confidence.score, 0) / patterns.length : 0;
|
|
777
|
+
const mostEffective = patterns.sort((a, b) => b.confidence.timesResolved - a.confidence.timesResolved).slice(0, 5).map((p) => ({ patternId: p.id, resolvedCount: p.confidence.timesResolved }));
|
|
778
|
+
const leastEffective = patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
|
|
779
|
+
patternId: p.id,
|
|
780
|
+
recurrenceRate: p.confidence.timesRecurred / Math.max(1, p.confidence.timesResolved)
|
|
781
|
+
})).sort((a, b) => b.recurrenceRate - a.recurrenceRate).slice(0, 5);
|
|
782
|
+
const symbolCounts = /* @__PURE__ */ new Map();
|
|
783
|
+
const incidents = this.getRecentIncidents({
|
|
784
|
+
dateFrom: start,
|
|
785
|
+
dateTo: end,
|
|
786
|
+
limit: 1e3
|
|
787
|
+
});
|
|
788
|
+
for (const incident of incidents) {
|
|
789
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
790
|
+
if (value) {
|
|
791
|
+
symbolCounts.set(value, (symbolCounts.get(value) || 0) + 1);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const mostIncidents = Array.from(symbolCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([symbol, count]) => ({ symbol, count }));
|
|
796
|
+
const resolutions = this.getResolutionHistory({ limit: 1e3 });
|
|
797
|
+
const periodResolutions = resolutions.filter(
|
|
798
|
+
(r) => r.resolvedAt >= start && r.resolvedAt <= end
|
|
799
|
+
);
|
|
800
|
+
const resolvedWithPattern = periodResolutions.filter(
|
|
801
|
+
(r) => r.patternId
|
|
802
|
+
).length;
|
|
803
|
+
const resolvedManually = periodResolutions.length - resolvedWithPattern;
|
|
804
|
+
return {
|
|
805
|
+
period: { start, end },
|
|
806
|
+
incidents: {
|
|
807
|
+
total,
|
|
808
|
+
open,
|
|
809
|
+
resolved,
|
|
810
|
+
byEnvironment,
|
|
811
|
+
byDay
|
|
812
|
+
},
|
|
813
|
+
patterns: {
|
|
814
|
+
total: patterns.length,
|
|
815
|
+
avgConfidence: Math.round(avgConfidence),
|
|
816
|
+
mostEffective,
|
|
817
|
+
leastEffective
|
|
818
|
+
},
|
|
819
|
+
symbols: {
|
|
820
|
+
mostIncidents,
|
|
821
|
+
mostResolved: [],
|
|
822
|
+
hotspots: mostIncidents.slice(0, 5).map((s) => ({
|
|
823
|
+
symbol: s.symbol,
|
|
824
|
+
incidentRate: s.count / Math.max(1, total)
|
|
825
|
+
}))
|
|
826
|
+
},
|
|
827
|
+
resolution: {
|
|
828
|
+
avgTimeToResolve: 0,
|
|
829
|
+
resolvedWithPattern,
|
|
830
|
+
resolvedManually,
|
|
831
|
+
resolutionRate: total > 0 ? resolved / total * 100 : 0
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
getSymbolHealth(symbol) {
|
|
836
|
+
const incidents = this.getRecentIncidents({ symbol, limit: 1e3 });
|
|
837
|
+
const incidentCount = incidents.length;
|
|
838
|
+
const patternCounts = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const incident of incidents) {
|
|
840
|
+
if (incident.resolution?.patternId) {
|
|
841
|
+
const count = patternCounts.get(incident.resolution.patternId) || 0;
|
|
842
|
+
patternCounts.set(incident.resolution.patternId, count + 1);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const topPatterns = Array.from(patternCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([patternId, count]) => ({ patternId, count }));
|
|
846
|
+
return {
|
|
847
|
+
incidentCount,
|
|
848
|
+
avgTimeToResolve: 0,
|
|
849
|
+
topPatterns
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
// ─── Import/Export ───────────────────────────────────────────────
|
|
853
|
+
exportPatterns(options = {}) {
|
|
854
|
+
const patterns = this.getAllPatterns({
|
|
855
|
+
includePrivate: options.includePrivate
|
|
856
|
+
});
|
|
857
|
+
return {
|
|
858
|
+
version: "1.0.0",
|
|
859
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
860
|
+
patterns
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
importPatterns(data, options = {}) {
|
|
864
|
+
let imported = 0;
|
|
865
|
+
let skipped = 0;
|
|
866
|
+
for (const pattern of data.patterns) {
|
|
867
|
+
const existing = this.getPattern(pattern.id);
|
|
868
|
+
if (existing && !options.overwrite) {
|
|
869
|
+
skipped++;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (existing) {
|
|
873
|
+
this.updatePattern(pattern.id, pattern);
|
|
874
|
+
} else {
|
|
875
|
+
this.addPattern({
|
|
876
|
+
...pattern,
|
|
877
|
+
source: "imported"
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
imported++;
|
|
881
|
+
}
|
|
882
|
+
return { imported, skipped };
|
|
883
|
+
}
|
|
884
|
+
exportBackup() {
|
|
885
|
+
const incidents = this.getRecentIncidents({ limit: 1e5 });
|
|
886
|
+
const patterns = this.getAllPatterns({ includePrivate: true });
|
|
887
|
+
const groups = this.getGroups({ limit: 1e4 });
|
|
888
|
+
return {
|
|
889
|
+
version: "1.0.0",
|
|
890
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
891
|
+
incidents,
|
|
892
|
+
patterns,
|
|
893
|
+
groups
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
importBackup(data) {
|
|
897
|
+
this.initializeSync();
|
|
898
|
+
this.db.run("DELETE FROM group_members");
|
|
899
|
+
this.db.run("DELETE FROM resolutions");
|
|
900
|
+
this.db.run("DELETE FROM groups");
|
|
901
|
+
this.db.run("DELETE FROM incidents");
|
|
902
|
+
this.db.run("DELETE FROM patterns");
|
|
903
|
+
for (const pattern of data.patterns) {
|
|
904
|
+
this.addPattern(pattern);
|
|
905
|
+
}
|
|
906
|
+
for (const incident of data.incidents) {
|
|
907
|
+
const now2 = incident.timestamp;
|
|
908
|
+
this.db.run(
|
|
909
|
+
`INSERT INTO incidents (
|
|
910
|
+
id, timestamp, status, error_message, error_stack, error_code, error_type,
|
|
911
|
+
symbols, flow_position, environment, service, version, user_id, request_id,
|
|
912
|
+
group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
|
|
913
|
+
created_at, updated_at
|
|
914
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
915
|
+
[
|
|
916
|
+
incident.id,
|
|
917
|
+
incident.timestamp,
|
|
918
|
+
incident.status,
|
|
919
|
+
incident.error.message,
|
|
920
|
+
incident.error.stack || null,
|
|
921
|
+
incident.error.code || null,
|
|
922
|
+
incident.error.type || null,
|
|
923
|
+
JSON.stringify(incident.symbols),
|
|
924
|
+
incident.flowPosition ? JSON.stringify(incident.flowPosition) : null,
|
|
925
|
+
incident.environment,
|
|
926
|
+
incident.service || null,
|
|
927
|
+
incident.version || null,
|
|
928
|
+
incident.userId || null,
|
|
929
|
+
incident.requestId || null,
|
|
930
|
+
incident.groupId || null,
|
|
931
|
+
JSON.stringify(incident.notes),
|
|
932
|
+
JSON.stringify(incident.relatedIncidents),
|
|
933
|
+
incident.resolvedAt || null,
|
|
934
|
+
incident.resolvedBy || null,
|
|
935
|
+
incident.resolution ? JSON.stringify(incident.resolution) : null,
|
|
936
|
+
now2,
|
|
937
|
+
now2
|
|
938
|
+
]
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const result = this.db.exec(
|
|
942
|
+
"SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
|
|
943
|
+
);
|
|
944
|
+
if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
|
|
945
|
+
this.incidentCounter = result[0].values[0][0];
|
|
946
|
+
}
|
|
947
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
948
|
+
for (const group of data.groups) {
|
|
949
|
+
this.db.run(
|
|
950
|
+
`INSERT INTO groups (
|
|
951
|
+
id, name, common_symbols, common_error_patterns,
|
|
952
|
+
suggested_pattern_id, first_seen, last_seen, created_at, updated_at
|
|
953
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
954
|
+
[
|
|
955
|
+
group.id,
|
|
956
|
+
group.name || null,
|
|
957
|
+
JSON.stringify(group.commonSymbols),
|
|
958
|
+
JSON.stringify(group.commonErrorPatterns),
|
|
959
|
+
group.suggestedPattern?.id || null,
|
|
960
|
+
group.firstSeen,
|
|
961
|
+
group.lastSeen,
|
|
962
|
+
now,
|
|
963
|
+
now
|
|
964
|
+
]
|
|
965
|
+
);
|
|
966
|
+
for (const incidentId of group.incidents) {
|
|
967
|
+
this.db.run(
|
|
968
|
+
"INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
|
|
969
|
+
[group.id, incidentId, now]
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
this.save();
|
|
974
|
+
}
|
|
975
|
+
// ─── Helper Methods ──────────────────────────────────────────────
|
|
976
|
+
rowToIncident(columns, row) {
|
|
977
|
+
const obj = {};
|
|
978
|
+
columns.forEach((col, i) => {
|
|
979
|
+
obj[col] = row[i];
|
|
980
|
+
});
|
|
981
|
+
return {
|
|
982
|
+
id: obj.id,
|
|
983
|
+
timestamp: obj.timestamp,
|
|
984
|
+
status: obj.status,
|
|
985
|
+
error: {
|
|
986
|
+
message: obj.error_message,
|
|
987
|
+
stack: obj.error_stack || void 0,
|
|
988
|
+
code: obj.error_code || void 0,
|
|
989
|
+
type: obj.error_type || void 0
|
|
990
|
+
},
|
|
991
|
+
symbols: JSON.parse(obj.symbols || "{}"),
|
|
992
|
+
flowPosition: obj.flow_position ? JSON.parse(obj.flow_position) : void 0,
|
|
993
|
+
environment: obj.environment,
|
|
994
|
+
service: obj.service || void 0,
|
|
995
|
+
version: obj.version || void 0,
|
|
996
|
+
userId: obj.user_id || void 0,
|
|
997
|
+
requestId: obj.request_id || void 0,
|
|
998
|
+
groupId: obj.group_id || void 0,
|
|
999
|
+
notes: JSON.parse(obj.notes || "[]"),
|
|
1000
|
+
relatedIncidents: JSON.parse(obj.related_incidents || "[]"),
|
|
1001
|
+
resolvedAt: obj.resolved_at || void 0,
|
|
1002
|
+
resolvedBy: obj.resolved_by || void 0,
|
|
1003
|
+
resolution: obj.resolution ? JSON.parse(obj.resolution) : void 0
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
rowToPattern(columns, row) {
|
|
1007
|
+
const obj = {};
|
|
1008
|
+
columns.forEach((col, i) => {
|
|
1009
|
+
obj[col] = row[i];
|
|
1010
|
+
});
|
|
1011
|
+
return {
|
|
1012
|
+
id: obj.id,
|
|
1013
|
+
name: obj.name,
|
|
1014
|
+
description: obj.description || "",
|
|
1015
|
+
pattern: JSON.parse(obj.pattern),
|
|
1016
|
+
resolution: JSON.parse(obj.resolution),
|
|
1017
|
+
confidence: JSON.parse(obj.confidence),
|
|
1018
|
+
source: obj.source,
|
|
1019
|
+
private: obj.private === 1,
|
|
1020
|
+
tags: JSON.parse(obj.tags || "[]"),
|
|
1021
|
+
createdAt: obj.created_at,
|
|
1022
|
+
updatedAt: obj.updated_at
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
rowToGroup(columns, row) {
|
|
1026
|
+
const obj = {};
|
|
1027
|
+
columns.forEach((col, i) => {
|
|
1028
|
+
obj[col] = row[i];
|
|
1029
|
+
});
|
|
1030
|
+
const membersResult = this.db.exec(
|
|
1031
|
+
"SELECT incident_id FROM group_members WHERE group_id = ?",
|
|
1032
|
+
[obj.id]
|
|
1033
|
+
);
|
|
1034
|
+
const incidents = [];
|
|
1035
|
+
if (membersResult.length > 0) {
|
|
1036
|
+
for (const r of membersResult[0].values) {
|
|
1037
|
+
incidents.push(r[0]);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const envResult = this.db.exec(
|
|
1041
|
+
`SELECT DISTINCT environment FROM incidents
|
|
1042
|
+
WHERE id IN (SELECT incident_id FROM group_members WHERE group_id = ?)`,
|
|
1043
|
+
[obj.id]
|
|
1044
|
+
);
|
|
1045
|
+
const environments = [];
|
|
1046
|
+
if (envResult.length > 0) {
|
|
1047
|
+
for (const r of envResult[0].values) {
|
|
1048
|
+
environments.push(r[0]);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
id: obj.id,
|
|
1053
|
+
name: obj.name || void 0,
|
|
1054
|
+
incidents,
|
|
1055
|
+
commonSymbols: JSON.parse(obj.common_symbols || "{}"),
|
|
1056
|
+
commonErrorPatterns: JSON.parse(
|
|
1057
|
+
obj.common_error_patterns || "[]"
|
|
1058
|
+
),
|
|
1059
|
+
count: incidents.length,
|
|
1060
|
+
firstSeen: obj.first_seen,
|
|
1061
|
+
lastSeen: obj.last_seen,
|
|
1062
|
+
environments,
|
|
1063
|
+
suggestedPattern: obj.suggested_pattern_id ? this.getPattern(obj.suggested_pattern_id) || void 0 : void 0
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
// ─── Practice Events ─────────────────────────────────────────────
|
|
1067
|
+
recordPracticeEvent(input) {
|
|
1068
|
+
this.initializeSync();
|
|
1069
|
+
const id = `PE-${uuidv4().substring(0, 8)}`;
|
|
1070
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1071
|
+
this.db.run(
|
|
1072
|
+
`INSERT INTO practice_events (
|
|
1073
|
+
id, timestamp, habit_id, habit_category, result,
|
|
1074
|
+
engineer, session_id, lore_entry_id, task_description,
|
|
1075
|
+
symbols_touched, files_modified, related_incident_id, notes
|
|
1076
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1077
|
+
[
|
|
1078
|
+
id,
|
|
1079
|
+
now,
|
|
1080
|
+
input.habitId,
|
|
1081
|
+
input.habitCategory,
|
|
1082
|
+
input.result,
|
|
1083
|
+
input.engineer,
|
|
1084
|
+
input.sessionId,
|
|
1085
|
+
input.loreEntryId || null,
|
|
1086
|
+
input.taskDescription || null,
|
|
1087
|
+
JSON.stringify(input.symbolsTouched || []),
|
|
1088
|
+
JSON.stringify(input.filesModified || []),
|
|
1089
|
+
input.relatedIncidentId || null,
|
|
1090
|
+
input.notes || null
|
|
1091
|
+
]
|
|
1092
|
+
);
|
|
1093
|
+
this.save();
|
|
1094
|
+
return id;
|
|
1095
|
+
}
|
|
1096
|
+
getPracticeEvents(options = {}) {
|
|
1097
|
+
this.initializeSync();
|
|
1098
|
+
const { limit = 100, offset = 0 } = options;
|
|
1099
|
+
const conditions = [];
|
|
1100
|
+
const params = [];
|
|
1101
|
+
if (options.habitId) {
|
|
1102
|
+
conditions.push("habit_id = ?");
|
|
1103
|
+
params.push(options.habitId);
|
|
1104
|
+
}
|
|
1105
|
+
if (options.habitCategory) {
|
|
1106
|
+
conditions.push("habit_category = ?");
|
|
1107
|
+
params.push(options.habitCategory);
|
|
1108
|
+
}
|
|
1109
|
+
if (options.result) {
|
|
1110
|
+
conditions.push("result = ?");
|
|
1111
|
+
params.push(options.result);
|
|
1112
|
+
}
|
|
1113
|
+
if (options.engineer) {
|
|
1114
|
+
conditions.push("engineer = ?");
|
|
1115
|
+
params.push(options.engineer);
|
|
1116
|
+
}
|
|
1117
|
+
if (options.sessionId) {
|
|
1118
|
+
conditions.push("session_id = ?");
|
|
1119
|
+
params.push(options.sessionId);
|
|
1120
|
+
}
|
|
1121
|
+
if (options.dateFrom) {
|
|
1122
|
+
conditions.push("timestamp >= ?");
|
|
1123
|
+
params.push(options.dateFrom);
|
|
1124
|
+
}
|
|
1125
|
+
if (options.dateTo) {
|
|
1126
|
+
conditions.push("timestamp <= ?");
|
|
1127
|
+
params.push(options.dateTo);
|
|
1128
|
+
}
|
|
1129
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1130
|
+
const result = this.db.exec(
|
|
1131
|
+
`SELECT * FROM practice_events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1132
|
+
[...params, limit, offset]
|
|
1133
|
+
);
|
|
1134
|
+
if (result.length === 0) return [];
|
|
1135
|
+
return result[0].values.map(
|
|
1136
|
+
(row) => this.rowToPracticeEvent(result[0].columns, row)
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
getPracticeEventCount(options = {}) {
|
|
1140
|
+
this.initializeSync();
|
|
1141
|
+
const conditions = [];
|
|
1142
|
+
const params = [];
|
|
1143
|
+
if (options.habitId) {
|
|
1144
|
+
conditions.push("habit_id = ?");
|
|
1145
|
+
params.push(options.habitId);
|
|
1146
|
+
}
|
|
1147
|
+
if (options.habitCategory) {
|
|
1148
|
+
conditions.push("habit_category = ?");
|
|
1149
|
+
params.push(options.habitCategory);
|
|
1150
|
+
}
|
|
1151
|
+
if (options.result) {
|
|
1152
|
+
conditions.push("result = ?");
|
|
1153
|
+
params.push(options.result);
|
|
1154
|
+
}
|
|
1155
|
+
if (options.engineer) {
|
|
1156
|
+
conditions.push("engineer = ?");
|
|
1157
|
+
params.push(options.engineer);
|
|
1158
|
+
}
|
|
1159
|
+
if (options.dateFrom) {
|
|
1160
|
+
conditions.push("timestamp >= ?");
|
|
1161
|
+
params.push(options.dateFrom);
|
|
1162
|
+
}
|
|
1163
|
+
if (options.dateTo) {
|
|
1164
|
+
conditions.push("timestamp <= ?");
|
|
1165
|
+
params.push(options.dateTo);
|
|
1166
|
+
}
|
|
1167
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1168
|
+
const result = this.db.exec(
|
|
1169
|
+
`SELECT COUNT(*) as count FROM practice_events ${whereClause}`,
|
|
1170
|
+
params
|
|
1171
|
+
);
|
|
1172
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1173
|
+
return result[0].values[0][0];
|
|
1174
|
+
}
|
|
1175
|
+
getComplianceRate(options = {}) {
|
|
1176
|
+
this.initializeSync();
|
|
1177
|
+
const conditions = [];
|
|
1178
|
+
const params = [];
|
|
1179
|
+
if (options.habitId) {
|
|
1180
|
+
conditions.push("habit_id = ?");
|
|
1181
|
+
params.push(options.habitId);
|
|
1182
|
+
}
|
|
1183
|
+
if (options.habitCategory) {
|
|
1184
|
+
conditions.push("habit_category = ?");
|
|
1185
|
+
params.push(options.habitCategory);
|
|
1186
|
+
}
|
|
1187
|
+
if (options.engineer) {
|
|
1188
|
+
conditions.push("engineer = ?");
|
|
1189
|
+
params.push(options.engineer);
|
|
1190
|
+
}
|
|
1191
|
+
if (options.dateFrom) {
|
|
1192
|
+
conditions.push("timestamp >= ?");
|
|
1193
|
+
params.push(options.dateFrom);
|
|
1194
|
+
}
|
|
1195
|
+
if (options.dateTo) {
|
|
1196
|
+
conditions.push("timestamp <= ?");
|
|
1197
|
+
params.push(options.dateTo);
|
|
1198
|
+
}
|
|
1199
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1200
|
+
const result = this.db.exec(
|
|
1201
|
+
`SELECT result, COUNT(*) as count
|
|
1202
|
+
FROM practice_events ${whereClause}
|
|
1203
|
+
GROUP BY result`,
|
|
1204
|
+
params
|
|
1205
|
+
);
|
|
1206
|
+
let followed = 0;
|
|
1207
|
+
let skipped = 0;
|
|
1208
|
+
let partial = 0;
|
|
1209
|
+
if (result.length > 0) {
|
|
1210
|
+
for (const row of result[0].values) {
|
|
1211
|
+
const r = row[0];
|
|
1212
|
+
const count = row[1];
|
|
1213
|
+
if (r === "followed") followed = count;
|
|
1214
|
+
else if (r === "skipped") skipped = count;
|
|
1215
|
+
else if (r === "partial") partial = count;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const total = followed + skipped + partial;
|
|
1219
|
+
const rate = total > 0 ? (followed + partial * 0.5) / total * 100 : 100;
|
|
1220
|
+
return { total, followed, skipped, partial, rate: Math.round(rate) };
|
|
1221
|
+
}
|
|
1222
|
+
rowToPracticeEvent(columns, row) {
|
|
1223
|
+
const obj = {};
|
|
1224
|
+
columns.forEach((col, i) => {
|
|
1225
|
+
obj[col] = row[i];
|
|
1226
|
+
});
|
|
1227
|
+
return {
|
|
1228
|
+
id: obj.id,
|
|
1229
|
+
timestamp: obj.timestamp,
|
|
1230
|
+
habitId: obj.habit_id,
|
|
1231
|
+
habitCategory: obj.habit_category,
|
|
1232
|
+
result: obj.result,
|
|
1233
|
+
engineer: obj.engineer,
|
|
1234
|
+
sessionId: obj.session_id,
|
|
1235
|
+
loreEntryId: obj.lore_entry_id || void 0,
|
|
1236
|
+
taskDescription: obj.task_description || void 0,
|
|
1237
|
+
symbolsTouched: JSON.parse(obj.symbols_touched || "[]"),
|
|
1238
|
+
filesModified: JSON.parse(obj.files_modified || "[]"),
|
|
1239
|
+
relatedIncidentId: obj.related_incident_id || void 0,
|
|
1240
|
+
notes: obj.notes || void 0
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
close() {
|
|
1244
|
+
if (this.db) {
|
|
1245
|
+
this.save();
|
|
1246
|
+
this.db.close();
|
|
1247
|
+
this.db = null;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// src/matcher.ts
|
|
1253
|
+
var DEFAULT_CONFIG = {
|
|
1254
|
+
minScore: 30,
|
|
1255
|
+
maxResults: 5,
|
|
1256
|
+
boostConfidence: true
|
|
1257
|
+
};
|
|
1258
|
+
var PatternMatcher = class {
|
|
1259
|
+
constructor(storage) {
|
|
1260
|
+
this.storage = storage;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Match an incident against all patterns and return ranked results
|
|
1264
|
+
*/
|
|
1265
|
+
match(incident, config = {}) {
|
|
1266
|
+
const { minScore, maxResults, boostConfidence } = {
|
|
1267
|
+
...DEFAULT_CONFIG,
|
|
1268
|
+
...config
|
|
1269
|
+
};
|
|
1270
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
1271
|
+
const matches = [];
|
|
1272
|
+
for (const pattern of patterns) {
|
|
1273
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
const { score, matchedCriteria } = this.scoreMatch(pattern, incident);
|
|
1277
|
+
if (score >= minScore) {
|
|
1278
|
+
let confidence = score;
|
|
1279
|
+
if (boostConfidence) {
|
|
1280
|
+
const confidenceFactor = pattern.confidence.score / 100;
|
|
1281
|
+
confidence = score * (0.5 + 0.5 * confidenceFactor);
|
|
1282
|
+
}
|
|
1283
|
+
matches.push({
|
|
1284
|
+
pattern,
|
|
1285
|
+
score,
|
|
1286
|
+
matchedCriteria,
|
|
1287
|
+
confidence: Math.round(confidence)
|
|
1288
|
+
});
|
|
1289
|
+
this.storage.updatePatternConfidence(pattern.id, "matched");
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return matches.sort((a, b) => b.confidence - a.confidence).slice(0, maxResults);
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Test a pattern against historical incidents
|
|
1296
|
+
*/
|
|
1297
|
+
testPattern(pattern, limit = 100) {
|
|
1298
|
+
const incidents = this.storage.getRecentIncidents({ limit });
|
|
1299
|
+
const wouldMatch = [];
|
|
1300
|
+
let totalScore = 0;
|
|
1301
|
+
for (const incident of incidents) {
|
|
1302
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
const { score } = this.scoreMatch(pattern, incident);
|
|
1306
|
+
if (score >= 30) {
|
|
1307
|
+
wouldMatch.push(incident);
|
|
1308
|
+
totalScore += score;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
wouldMatch,
|
|
1313
|
+
matchCount: wouldMatch.length,
|
|
1314
|
+
avgScore: wouldMatch.length > 0 ? Math.round(totalScore / wouldMatch.length) : 0
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Score how well a pattern matches an incident
|
|
1319
|
+
*/
|
|
1320
|
+
scoreMatch(pattern, incident) {
|
|
1321
|
+
let score = 0;
|
|
1322
|
+
const matchedCriteria = {
|
|
1323
|
+
symbols: [],
|
|
1324
|
+
errorKeywords: [],
|
|
1325
|
+
missingSignals: []
|
|
1326
|
+
};
|
|
1327
|
+
const symbolScore = this.matchSymbols(
|
|
1328
|
+
pattern.pattern.symbols,
|
|
1329
|
+
incident.symbols,
|
|
1330
|
+
matchedCriteria.symbols
|
|
1331
|
+
);
|
|
1332
|
+
score += Math.min(symbolScore, 50);
|
|
1333
|
+
const errorScore = this.matchErrorText(
|
|
1334
|
+
pattern,
|
|
1335
|
+
incident,
|
|
1336
|
+
matchedCriteria.errorKeywords
|
|
1337
|
+
);
|
|
1338
|
+
score += Math.min(errorScore, 25);
|
|
1339
|
+
const signalScore = this.matchMissingSignals(
|
|
1340
|
+
pattern,
|
|
1341
|
+
incident,
|
|
1342
|
+
matchedCriteria.missingSignals
|
|
1343
|
+
);
|
|
1344
|
+
score += Math.min(signalScore, 25);
|
|
1345
|
+
score = Math.min(score, 100);
|
|
1346
|
+
return { score, matchedCriteria };
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Match symbols between pattern and incident
|
|
1350
|
+
*/
|
|
1351
|
+
matchSymbols(patternSymbols, incidentSymbols, matched) {
|
|
1352
|
+
let score = 0;
|
|
1353
|
+
const symbolTypes = [
|
|
1354
|
+
"feature",
|
|
1355
|
+
"component",
|
|
1356
|
+
"flow",
|
|
1357
|
+
"gate",
|
|
1358
|
+
"signal",
|
|
1359
|
+
"state",
|
|
1360
|
+
"integration"
|
|
1361
|
+
];
|
|
1362
|
+
for (const type of symbolTypes) {
|
|
1363
|
+
const patternValue = patternSymbols[type];
|
|
1364
|
+
const incidentValue = incidentSymbols[type];
|
|
1365
|
+
if (!patternValue || !incidentValue) {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof patternValue === "string") {
|
|
1369
|
+
if (this.matchSingleSymbol(patternValue, incidentValue)) {
|
|
1370
|
+
score += patternValue.includes("*") ? 5 : 10;
|
|
1371
|
+
matched.push(type);
|
|
1372
|
+
}
|
|
1373
|
+
} else if (Array.isArray(patternValue)) {
|
|
1374
|
+
for (const pv of patternValue) {
|
|
1375
|
+
if (this.matchSingleSymbol(pv, incidentValue)) {
|
|
1376
|
+
score += 7;
|
|
1377
|
+
matched.push(type);
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return score;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Match a single symbol value (supports wildcards)
|
|
1387
|
+
*/
|
|
1388
|
+
matchSingleSymbol(pattern, value) {
|
|
1389
|
+
if (pattern === "*") {
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
if (pattern.endsWith("*")) {
|
|
1393
|
+
const prefix = pattern.slice(0, -1);
|
|
1394
|
+
return value.startsWith(prefix);
|
|
1395
|
+
}
|
|
1396
|
+
if (pattern.startsWith("*")) {
|
|
1397
|
+
const suffix = pattern.slice(1);
|
|
1398
|
+
return value.endsWith(suffix);
|
|
1399
|
+
}
|
|
1400
|
+
if (pattern.includes("*")) {
|
|
1401
|
+
const regex = new RegExp(
|
|
1402
|
+
"^" + pattern.replace(/\*/g, ".*") + "$"
|
|
1403
|
+
);
|
|
1404
|
+
return regex.test(value);
|
|
1405
|
+
}
|
|
1406
|
+
return pattern === value;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Match error text keywords and regex
|
|
1410
|
+
*/
|
|
1411
|
+
matchErrorText(pattern, incident, matched) {
|
|
1412
|
+
let score = 0;
|
|
1413
|
+
const errorMessage = incident.error.message.toLowerCase();
|
|
1414
|
+
const errorType = incident.error.type?.toLowerCase();
|
|
1415
|
+
if (pattern.pattern.errorContains) {
|
|
1416
|
+
for (const keyword of pattern.pattern.errorContains) {
|
|
1417
|
+
if (errorMessage.includes(keyword.toLowerCase())) {
|
|
1418
|
+
score += 5;
|
|
1419
|
+
matched.push(keyword);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (pattern.pattern.errorMatches) {
|
|
1424
|
+
try {
|
|
1425
|
+
const regex = new RegExp(pattern.pattern.errorMatches, "i");
|
|
1426
|
+
if (regex.test(incident.error.message)) {
|
|
1427
|
+
score += 10;
|
|
1428
|
+
matched.push(`regex:${pattern.pattern.errorMatches}`);
|
|
1429
|
+
}
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (pattern.pattern.errorType && errorType) {
|
|
1434
|
+
for (const type of pattern.pattern.errorType) {
|
|
1435
|
+
if (errorType.includes(type.toLowerCase())) {
|
|
1436
|
+
score += 5;
|
|
1437
|
+
matched.push(`type:${type}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return score;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Match missing signals from flow position
|
|
1445
|
+
*/
|
|
1446
|
+
matchMissingSignals(pattern, incident, matched) {
|
|
1447
|
+
if (!pattern.pattern.missingSignals || !incident.flowPosition?.missing) {
|
|
1448
|
+
return 0;
|
|
1449
|
+
}
|
|
1450
|
+
let score = 0;
|
|
1451
|
+
for (const expectedSignal of pattern.pattern.missingSignals) {
|
|
1452
|
+
for (const missingSignal of incident.flowPosition.missing) {
|
|
1453
|
+
if (this.matchSingleSymbol(expectedSignal, missingSignal)) {
|
|
1454
|
+
score += 12;
|
|
1455
|
+
matched.push(missingSignal);
|
|
1456
|
+
break;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return score;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Check if pattern's environment filter matches incident
|
|
1464
|
+
*/
|
|
1465
|
+
matchEnvironment(pattern, incident) {
|
|
1466
|
+
if (!pattern.pattern.environment || pattern.pattern.environment.length === 0) {
|
|
1467
|
+
return true;
|
|
1468
|
+
}
|
|
1469
|
+
return pattern.pattern.environment.includes(incident.environment);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
// src/stats.ts
|
|
1474
|
+
var StatsCalculator = class {
|
|
1475
|
+
constructor(storage) {
|
|
1476
|
+
this.storage = storage;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Get comprehensive statistics for a time period
|
|
1480
|
+
*/
|
|
1481
|
+
getStats(periodDays = 7) {
|
|
1482
|
+
const end = (/* @__PURE__ */ new Date()).toISOString();
|
|
1483
|
+
const start = new Date(
|
|
1484
|
+
Date.now() - periodDays * 24 * 60 * 60 * 1e3
|
|
1485
|
+
).toISOString();
|
|
1486
|
+
return this.storage.getStats({ start, end });
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Get health metrics for a specific symbol
|
|
1490
|
+
*/
|
|
1491
|
+
getSymbolHealth(symbol) {
|
|
1492
|
+
return this.storage.getSymbolHealth(symbol);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Get trending issues (symbols with increasing incident rates)
|
|
1496
|
+
*/
|
|
1497
|
+
getTrendingIssues(days = 7) {
|
|
1498
|
+
const now = Date.now();
|
|
1499
|
+
const halfPeriod = days * 24 * 60 * 60 * 1e3 / 2;
|
|
1500
|
+
const firstHalfStart = new Date(now - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
1501
|
+
const midpoint = new Date(now - halfPeriod).toISOString();
|
|
1502
|
+
const secondHalfEnd = new Date(now).toISOString();
|
|
1503
|
+
const firstHalfIncidents = this.storage.getRecentIncidents({
|
|
1504
|
+
dateFrom: firstHalfStart,
|
|
1505
|
+
dateTo: midpoint,
|
|
1506
|
+
limit: 1e3
|
|
1507
|
+
});
|
|
1508
|
+
const secondHalfIncidents = this.storage.getRecentIncidents({
|
|
1509
|
+
dateFrom: midpoint,
|
|
1510
|
+
dateTo: secondHalfEnd,
|
|
1511
|
+
limit: 1e3
|
|
1512
|
+
});
|
|
1513
|
+
const firstHalfCounts = this.countSymbols(firstHalfIncidents);
|
|
1514
|
+
const secondHalfCounts = this.countSymbols(secondHalfIncidents);
|
|
1515
|
+
const trends = [];
|
|
1516
|
+
const allSymbols = /* @__PURE__ */ new Set([
|
|
1517
|
+
...firstHalfCounts.keys(),
|
|
1518
|
+
...secondHalfCounts.keys()
|
|
1519
|
+
]);
|
|
1520
|
+
for (const symbol of allSymbols) {
|
|
1521
|
+
const first = firstHalfCounts.get(symbol) || 0;
|
|
1522
|
+
const second = secondHalfCounts.get(symbol) || 0;
|
|
1523
|
+
if (first === 0 && second > 0) {
|
|
1524
|
+
trends.push({ symbol, trend: second * 100 });
|
|
1525
|
+
} else if (first > 0) {
|
|
1526
|
+
const change = (second - first) / first * 100;
|
|
1527
|
+
trends.push({ symbol, trend: change });
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return trends.filter((t) => t.trend > 0).sort((a, b) => b.trend - a.trend).slice(0, 10);
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Get resolution metrics
|
|
1534
|
+
*/
|
|
1535
|
+
getResolutionMetrics() {
|
|
1536
|
+
const stats = this.getStats(30);
|
|
1537
|
+
return {
|
|
1538
|
+
avgTimeToResolve: stats.resolution.avgTimeToResolve,
|
|
1539
|
+
resolvedWithPattern: stats.resolution.resolvedWithPattern,
|
|
1540
|
+
resolvedManually: stats.resolution.resolvedManually,
|
|
1541
|
+
totalResolved: stats.incidents.resolved,
|
|
1542
|
+
resolutionRate: stats.resolution.resolutionRate
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Get pattern effectiveness metrics
|
|
1547
|
+
*/
|
|
1548
|
+
getPatternEffectiveness() {
|
|
1549
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
1550
|
+
return patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
|
|
1551
|
+
patternId: p.id,
|
|
1552
|
+
name: p.name,
|
|
1553
|
+
matches: p.confidence.timesMatched,
|
|
1554
|
+
resolutions: p.confidence.timesResolved,
|
|
1555
|
+
recurrences: p.confidence.timesRecurred,
|
|
1556
|
+
effectiveness: p.confidence.timesMatched > 0 ? Math.round(
|
|
1557
|
+
(p.confidence.timesResolved - p.confidence.timesRecurred) / p.confidence.timesMatched * 100
|
|
1558
|
+
) : 0
|
|
1559
|
+
})).sort((a, b) => b.effectiveness - a.effectiveness);
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Get incident rate by hour of day
|
|
1563
|
+
*/
|
|
1564
|
+
getIncidentsByHour(days = 7) {
|
|
1565
|
+
const start = new Date(
|
|
1566
|
+
Date.now() - days * 24 * 60 * 60 * 1e3
|
|
1567
|
+
).toISOString();
|
|
1568
|
+
const incidents = this.storage.getRecentIncidents({
|
|
1569
|
+
dateFrom: start,
|
|
1570
|
+
limit: 1e4
|
|
1571
|
+
});
|
|
1572
|
+
const hourCounts = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (let i = 0; i < 24; i++) {
|
|
1574
|
+
hourCounts.set(i, 0);
|
|
1575
|
+
}
|
|
1576
|
+
for (const incident of incidents) {
|
|
1577
|
+
const hour = new Date(incident.timestamp).getHours();
|
|
1578
|
+
hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
|
|
1579
|
+
}
|
|
1580
|
+
return Array.from(hourCounts.entries()).map(([hour, count]) => ({
|
|
1581
|
+
hour,
|
|
1582
|
+
count
|
|
1583
|
+
}));
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Get incident rate by environment
|
|
1587
|
+
*/
|
|
1588
|
+
getIncidentsByEnvironment() {
|
|
1589
|
+
const stats = this.getStats(30);
|
|
1590
|
+
const total = stats.incidents.total;
|
|
1591
|
+
return Object.entries(stats.incidents.byEnvironment).map(([environment, count]) => ({
|
|
1592
|
+
environment,
|
|
1593
|
+
count,
|
|
1594
|
+
percentage: total > 0 ? Math.round(count / total * 100) : 0
|
|
1595
|
+
})).sort((a, b) => b.count - a.count);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Get symbol correlation matrix (which symbols fail together)
|
|
1599
|
+
*/
|
|
1600
|
+
getSymbolCorrelation() {
|
|
1601
|
+
const incidents = this.storage.getRecentIncidents({ limit: 1e3 });
|
|
1602
|
+
const correlations = /* @__PURE__ */ new Map();
|
|
1603
|
+
const symbolCounts = /* @__PURE__ */ new Map();
|
|
1604
|
+
for (const incident of incidents) {
|
|
1605
|
+
const symbols = this.getSymbolsFromIncident(incident);
|
|
1606
|
+
for (const symbol of symbols) {
|
|
1607
|
+
symbolCounts.set(symbol, (symbolCounts.get(symbol) || 0) + 1);
|
|
1608
|
+
}
|
|
1609
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1610
|
+
for (let j = i + 1; j < symbols.length; j++) {
|
|
1611
|
+
const key = [symbols[i], symbols[j]].sort().join("|");
|
|
1612
|
+
correlations.set(key, (correlations.get(key) || 0) + 1);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const results = [];
|
|
1617
|
+
for (const [key, count] of correlations) {
|
|
1618
|
+
const [symbol1, symbol2] = key.split("|");
|
|
1619
|
+
const count1 = symbolCounts.get(symbol1) || 1;
|
|
1620
|
+
const count2 = symbolCounts.get(symbol2) || 1;
|
|
1621
|
+
const correlation = count / Math.max(count1, count2);
|
|
1622
|
+
if (correlation > 0.3) {
|
|
1623
|
+
results.push({
|
|
1624
|
+
symbol1,
|
|
1625
|
+
symbol2,
|
|
1626
|
+
correlation: Math.round(correlation * 100) / 100
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return results.sort((a, b) => b.correlation - a.correlation).slice(0, 20);
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Generate a summary dashboard string
|
|
1634
|
+
*/
|
|
1635
|
+
generateDashboard(periodDays = 7) {
|
|
1636
|
+
const stats = this.getStats(periodDays);
|
|
1637
|
+
const lines = [];
|
|
1638
|
+
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");
|
|
1639
|
+
lines.push("\u2551 PARADIGM SENTINEL DASHBOARD \u2551");
|
|
1640
|
+
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");
|
|
1641
|
+
const todayCount = stats.incidents.byDay[stats.incidents.byDay.length - 1]?.count || 0;
|
|
1642
|
+
lines.push(
|
|
1643
|
+
`\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`
|
|
1644
|
+
);
|
|
1645
|
+
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");
|
|
1646
|
+
lines.push("");
|
|
1647
|
+
lines.push("Incidents by Day (last 7 days):");
|
|
1648
|
+
lines.push("\u2500".repeat(50));
|
|
1649
|
+
const maxDayCount = Math.max(...stats.incidents.byDay.map((d) => d.count), 1);
|
|
1650
|
+
for (const day of stats.incidents.byDay.slice(-7)) {
|
|
1651
|
+
const barLength = Math.round(day.count / maxDayCount * 30);
|
|
1652
|
+
const bar = "\u2588".repeat(barLength);
|
|
1653
|
+
lines.push(`${day.date.substring(5)} ${bar} ${day.count}`);
|
|
1654
|
+
}
|
|
1655
|
+
lines.push("");
|
|
1656
|
+
lines.push("Most Affected Symbols:");
|
|
1657
|
+
lines.push("\u2500".repeat(50));
|
|
1658
|
+
for (const { symbol, count } of stats.symbols.mostIncidents.slice(0, 5)) {
|
|
1659
|
+
lines.push(` ${symbol.padEnd(25)} ${count} incidents`);
|
|
1660
|
+
}
|
|
1661
|
+
lines.push("");
|
|
1662
|
+
lines.push("Top Patterns:");
|
|
1663
|
+
lines.push("\u2500".repeat(50));
|
|
1664
|
+
for (const { patternId, resolvedCount } of stats.patterns.mostEffective.slice(0, 5)) {
|
|
1665
|
+
lines.push(` ${patternId.padEnd(25)} ${resolvedCount} resolved`);
|
|
1666
|
+
}
|
|
1667
|
+
lines.push("");
|
|
1668
|
+
lines.push("Resolution Stats:");
|
|
1669
|
+
lines.push("\u2500".repeat(50));
|
|
1670
|
+
lines.push(` Resolution rate: ${Math.round(stats.resolution.resolutionRate)}%`);
|
|
1671
|
+
lines.push(` With pattern: ${stats.resolution.resolvedWithPattern}`);
|
|
1672
|
+
lines.push(` Manual: ${stats.resolution.resolvedManually}`);
|
|
1673
|
+
return lines.join("\n");
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Helper: Count symbols across incidents
|
|
1677
|
+
*/
|
|
1678
|
+
countSymbols(incidents) {
|
|
1679
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1680
|
+
for (const incident of incidents) {
|
|
1681
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
1682
|
+
if (value) {
|
|
1683
|
+
counts.set(value, (counts.get(value) || 0) + 1);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return counts;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Helper: Get all symbols from incident
|
|
1691
|
+
*/
|
|
1692
|
+
getSymbolsFromIncident(incident) {
|
|
1693
|
+
const symbols = [];
|
|
1694
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
1695
|
+
if (value) {
|
|
1696
|
+
symbols.push(value);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return symbols;
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
// src/timeline.ts
|
|
1704
|
+
var TimelineBuilder = class {
|
|
1705
|
+
/**
|
|
1706
|
+
* Build a timeline from an incident with flow position
|
|
1707
|
+
*/
|
|
1708
|
+
build(incident) {
|
|
1709
|
+
if (!incident.flowPosition) {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
const events = [];
|
|
1713
|
+
const baseTime = new Date(incident.timestamp).getTime();
|
|
1714
|
+
events.push({
|
|
1715
|
+
timestamp: new Date(baseTime - 5e3).toISOString(),
|
|
1716
|
+
symbol: incident.flowPosition.flowId,
|
|
1717
|
+
type: "flow-started"
|
|
1718
|
+
});
|
|
1719
|
+
let eventOffset = 1e3;
|
|
1720
|
+
for (const signal of incident.flowPosition.actual) {
|
|
1721
|
+
const type = this.inferEventType(signal);
|
|
1722
|
+
events.push({
|
|
1723
|
+
timestamp: new Date(baseTime - 4e3 + eventOffset).toISOString(),
|
|
1724
|
+
symbol: signal,
|
|
1725
|
+
type
|
|
1726
|
+
});
|
|
1727
|
+
eventOffset += Math.random() * 1e3 + 500;
|
|
1728
|
+
}
|
|
1729
|
+
const failedSymbol = incident.flowPosition.failedAt || incident.flowPosition.missing[0] || incident.symbols.gate || incident.symbols.signal || "unknown";
|
|
1730
|
+
events.push({
|
|
1731
|
+
timestamp: incident.timestamp,
|
|
1732
|
+
symbol: failedSymbol,
|
|
1733
|
+
type: "error",
|
|
1734
|
+
data: {
|
|
1735
|
+
message: incident.error.message,
|
|
1736
|
+
missing: incident.flowPosition.missing
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
return {
|
|
1740
|
+
incidentId: incident.id,
|
|
1741
|
+
flowId: incident.flowPosition.flowId,
|
|
1742
|
+
events,
|
|
1743
|
+
failure: {
|
|
1744
|
+
at: incident.timestamp,
|
|
1745
|
+
symbol: failedSymbol,
|
|
1746
|
+
reason: incident.error.message
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Render timeline as ASCII art
|
|
1752
|
+
*/
|
|
1753
|
+
renderAscii(timeline) {
|
|
1754
|
+
const lines = [];
|
|
1755
|
+
lines.push(`${timeline.flowId} Timeline`);
|
|
1756
|
+
lines.push("\u2550".repeat(40));
|
|
1757
|
+
lines.push("");
|
|
1758
|
+
for (const event of timeline.events) {
|
|
1759
|
+
const time = this.formatTime(event.timestamp);
|
|
1760
|
+
const icon = this.getEventIcon(event.type);
|
|
1761
|
+
const status = this.getEventStatus(event.type);
|
|
1762
|
+
let line = `${time} ${icon} ${event.symbol}`;
|
|
1763
|
+
if (status) {
|
|
1764
|
+
line += ` (${status})`;
|
|
1765
|
+
}
|
|
1766
|
+
lines.push(line);
|
|
1767
|
+
if (event.type === "error" && event.data) {
|
|
1768
|
+
lines.push(` \u2514\u2500 ${event.data.message}`);
|
|
1769
|
+
if (event.data.missing && Array.isArray(event.data.missing) && event.data.missing.length > 0) {
|
|
1770
|
+
lines.push(
|
|
1771
|
+
` \u2514\u2500 Expected: ${event.data.missing.join(", ")}`
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
const missing = timeline.events.find((e) => e.type === "error")?.data?.missing;
|
|
1777
|
+
if (missing && missing.length > 0) {
|
|
1778
|
+
lines.push("");
|
|
1779
|
+
lines.push(`Missing signals: ${missing.join(", ")}`);
|
|
1780
|
+
}
|
|
1781
|
+
return lines.join("\n");
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Render timeline as structured data (for MCP/JSON output)
|
|
1785
|
+
*/
|
|
1786
|
+
renderStructured(timeline) {
|
|
1787
|
+
return {
|
|
1788
|
+
incidentId: timeline.incidentId,
|
|
1789
|
+
flow: {
|
|
1790
|
+
id: timeline.flowId,
|
|
1791
|
+
eventCount: timeline.events.length
|
|
1792
|
+
},
|
|
1793
|
+
events: timeline.events.map((event) => ({
|
|
1794
|
+
time: this.formatTime(event.timestamp),
|
|
1795
|
+
symbol: event.symbol,
|
|
1796
|
+
type: event.type,
|
|
1797
|
+
status: this.getEventStatus(event.type),
|
|
1798
|
+
data: event.data
|
|
1799
|
+
})),
|
|
1800
|
+
failure: {
|
|
1801
|
+
at: this.formatTime(timeline.failure.at),
|
|
1802
|
+
symbol: timeline.failure.symbol,
|
|
1803
|
+
reason: timeline.failure.reason
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Infer event type from symbol prefix
|
|
1809
|
+
*/
|
|
1810
|
+
inferEventType(symbol) {
|
|
1811
|
+
if (symbol.startsWith("^")) {
|
|
1812
|
+
return "gate-passed";
|
|
1813
|
+
}
|
|
1814
|
+
if (symbol.startsWith("!")) {
|
|
1815
|
+
return "signal-emitted";
|
|
1816
|
+
}
|
|
1817
|
+
if (symbol.startsWith("%")) {
|
|
1818
|
+
return "state-changed";
|
|
1819
|
+
}
|
|
1820
|
+
return "signal-emitted";
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Get icon for event type
|
|
1824
|
+
*/
|
|
1825
|
+
getEventIcon(type) {
|
|
1826
|
+
switch (type) {
|
|
1827
|
+
case "flow-started":
|
|
1828
|
+
return "\u25B6";
|
|
1829
|
+
case "flow-ended":
|
|
1830
|
+
return "\u25A0";
|
|
1831
|
+
case "gate-passed":
|
|
1832
|
+
return "\u2713";
|
|
1833
|
+
case "gate-failed":
|
|
1834
|
+
return "\u2717";
|
|
1835
|
+
case "signal-emitted":
|
|
1836
|
+
return "\u26A1";
|
|
1837
|
+
case "state-changed":
|
|
1838
|
+
return "\u25C6";
|
|
1839
|
+
case "error":
|
|
1840
|
+
return "\u2717";
|
|
1841
|
+
default:
|
|
1842
|
+
return "\u2022";
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Get status text for event type
|
|
1847
|
+
*/
|
|
1848
|
+
getEventStatus(type) {
|
|
1849
|
+
switch (type) {
|
|
1850
|
+
case "gate-passed":
|
|
1851
|
+
return "PASSED";
|
|
1852
|
+
case "gate-failed":
|
|
1853
|
+
return "FAILED";
|
|
1854
|
+
case "signal-emitted":
|
|
1855
|
+
return "EMITTED";
|
|
1856
|
+
case "state-changed":
|
|
1857
|
+
return "CHANGED";
|
|
1858
|
+
case "error":
|
|
1859
|
+
return "ERROR";
|
|
1860
|
+
default:
|
|
1861
|
+
return "";
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Format timestamp for display
|
|
1866
|
+
*/
|
|
1867
|
+
formatTime(timestamp) {
|
|
1868
|
+
const date = new Date(timestamp);
|
|
1869
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
1870
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
1871
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
1872
|
+
const millis = String(date.getMilliseconds()).padStart(3, "0");
|
|
1873
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
// src/seeds/loader.ts
|
|
1878
|
+
import * as path2 from "path";
|
|
1879
|
+
import * as fs2 from "fs";
|
|
1880
|
+
import { fileURLToPath } from "url";
|
|
1881
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1882
|
+
var __dirname = path2.dirname(__filename);
|
|
1883
|
+
function loadUniversalPatterns() {
|
|
1884
|
+
const filePath = path2.join(__dirname, "universal-patterns.json");
|
|
1885
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
1886
|
+
return JSON.parse(content);
|
|
1887
|
+
}
|
|
1888
|
+
function loadParadigmPatterns() {
|
|
1889
|
+
const filePath = path2.join(__dirname, "paradigm-patterns.json");
|
|
1890
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
1891
|
+
return JSON.parse(content);
|
|
1892
|
+
}
|
|
1893
|
+
function loadAllSeedPatterns() {
|
|
1894
|
+
const universal = loadUniversalPatterns();
|
|
1895
|
+
const paradigm = loadParadigmPatterns();
|
|
1896
|
+
return {
|
|
1897
|
+
version: "1.0.0",
|
|
1898
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1899
|
+
patterns: [...universal.patterns, ...paradigm.patterns]
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/server/index.ts
|
|
1904
|
+
import express from "express";
|
|
1905
|
+
import * as path5 from "path";
|
|
1906
|
+
import * as fs4 from "fs";
|
|
1907
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1908
|
+
import chalk3 from "chalk";
|
|
1909
|
+
|
|
1910
|
+
// src/server/routes/symbols.ts
|
|
1911
|
+
import { Router } from "express";
|
|
1912
|
+
import chalk2 from "chalk";
|
|
1913
|
+
|
|
1914
|
+
// src/server/loaders/symbols.ts
|
|
1915
|
+
import * as fs3 from "fs";
|
|
1916
|
+
import * as path3 from "path";
|
|
1917
|
+
import chalk from "chalk";
|
|
1918
|
+
var LOG_LEVEL = process.env.SENTINEL_LOG_LEVEL || process.env.LOG_LEVEL || "info";
|
|
1919
|
+
var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
1920
|
+
function shouldLog(level) {
|
|
1921
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[LOG_LEVEL];
|
|
1922
|
+
}
|
|
1923
|
+
function formatData(data) {
|
|
1924
|
+
if (!data) return "";
|
|
1925
|
+
const entries = Object.entries(data).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
|
|
1926
|
+
return chalk.gray(` ${entries}`);
|
|
1927
|
+
}
|
|
1928
|
+
var log = {
|
|
1929
|
+
component(name) {
|
|
1930
|
+
const symbol = chalk.magenta(`#${name}`);
|
|
1931
|
+
return {
|
|
1932
|
+
debug: (msg, data) => {
|
|
1933
|
+
if (shouldLog("debug")) console.log(`${chalk.gray("\u25CB")} ${symbol} ${msg}${formatData(data)}`);
|
|
1934
|
+
},
|
|
1935
|
+
info: (msg, data) => {
|
|
1936
|
+
if (shouldLog("info")) console.log(`${chalk.blue("\u2139")} ${symbol} ${msg}${formatData(data)}`);
|
|
1937
|
+
},
|
|
1938
|
+
warn: (msg, data) => {
|
|
1939
|
+
if (shouldLog("warn")) console.log(`${chalk.yellow("\u26A0")} ${symbol} ${msg}${formatData(data)}`);
|
|
1940
|
+
},
|
|
1941
|
+
error: (msg, data) => {
|
|
1942
|
+
if (shouldLog("error")) console.error(`${chalk.red("\u2716")} ${symbol} ${msg}${formatData(data)}`);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
},
|
|
1946
|
+
flow(name) {
|
|
1947
|
+
const symbol = chalk.yellow(`$${name}`);
|
|
1948
|
+
return {
|
|
1949
|
+
debug: (msg, data) => {
|
|
1950
|
+
if (shouldLog("debug")) console.log(`${chalk.gray("\u25CB")} ${symbol} ${msg}${formatData(data)}`);
|
|
1951
|
+
},
|
|
1952
|
+
info: (msg, data) => {
|
|
1953
|
+
if (shouldLog("info")) console.log(`${chalk.blue("\u2139")} ${symbol} ${msg}${formatData(data)}`);
|
|
1954
|
+
},
|
|
1955
|
+
warn: (msg, data) => {
|
|
1956
|
+
if (shouldLog("warn")) console.log(`${chalk.yellow("\u26A0")} ${symbol} ${msg}${formatData(data)}`);
|
|
1957
|
+
},
|
|
1958
|
+
error: (msg, data) => {
|
|
1959
|
+
if (shouldLog("error")) console.error(`${chalk.red("\u2716")} ${symbol} ${msg}${formatData(data)}`);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
var SYMBOL_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
1965
|
+
"$lib",
|
|
1966
|
+
"$env",
|
|
1967
|
+
"$app",
|
|
1968
|
+
"$service-worker",
|
|
1969
|
+
"$virtual",
|
|
1970
|
+
"$schema",
|
|
1971
|
+
"$ref",
|
|
1972
|
+
"$id",
|
|
1973
|
+
"$type"
|
|
1974
|
+
]);
|
|
1975
|
+
async function loadParadigmConfig(projectDir) {
|
|
1976
|
+
const configPath = path3.join(projectDir, ".paradigm", "config.yaml");
|
|
1977
|
+
if (!fs3.existsSync(configPath)) {
|
|
1978
|
+
const packagePath = path3.join(projectDir, "package.json");
|
|
1979
|
+
if (fs3.existsSync(packagePath)) {
|
|
1980
|
+
try {
|
|
1981
|
+
const pkg = JSON.parse(fs3.readFileSync(packagePath, "utf-8"));
|
|
1982
|
+
return { name: pkg.name };
|
|
1983
|
+
} catch {
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return {};
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
const content = fs3.readFileSync(configPath, "utf-8");
|
|
1990
|
+
const config = {};
|
|
1991
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
1992
|
+
if (nameMatch) config.name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
1993
|
+
const disciplineMatch = content.match(/^discipline:\s*(.+)$/m);
|
|
1994
|
+
if (disciplineMatch) config.discipline = disciplineMatch[1].trim();
|
|
1995
|
+
const versionMatch = content.match(/^version:\s*(.+)$/m);
|
|
1996
|
+
if (versionMatch) config.version = versionMatch[1].trim();
|
|
1997
|
+
return config;
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
log.component("config-loader").error("Failed to load Paradigm config", { error: String(error) });
|
|
2000
|
+
return {};
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
async function loadWithPremiseCore(projectDir) {
|
|
2004
|
+
try {
|
|
2005
|
+
const { aggregateFromDirectory } = await import("./dist-BPWLYV4U.js");
|
|
2006
|
+
log.flow("load-symbols").info("Using premise-core aggregator", { path: projectDir });
|
|
2007
|
+
const result = await aggregateFromDirectory(projectDir);
|
|
2008
|
+
const counts = {};
|
|
2009
|
+
for (const sym of result.symbols) {
|
|
2010
|
+
counts[sym.type] = (counts[sym.type] || 0) + 1;
|
|
2011
|
+
}
|
|
2012
|
+
log.flow("load-symbols").info("Aggregation complete", {
|
|
2013
|
+
total: result.symbols.length,
|
|
2014
|
+
...counts,
|
|
2015
|
+
purposeFiles: result.purposeFiles.length,
|
|
2016
|
+
portalFiles: result.portalFiles.length
|
|
2017
|
+
});
|
|
2018
|
+
if (result.errors.length > 0) {
|
|
2019
|
+
for (const err of result.errors) {
|
|
2020
|
+
log.component("aggregator").warn("Aggregation error", {
|
|
2021
|
+
source: err.source,
|
|
2022
|
+
file: err.filePath,
|
|
2023
|
+
message: err.message
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
for (const file of result.purposeFiles) {
|
|
2028
|
+
log.component("purpose-loader").info("Loaded .purpose file", { file: path3.relative(projectDir, file) });
|
|
2029
|
+
}
|
|
2030
|
+
for (const file of result.portalFiles) {
|
|
2031
|
+
log.component("gate-loader").info("Loaded portal.yaml", { file: path3.relative(projectDir, file) });
|
|
2032
|
+
}
|
|
2033
|
+
return result.symbols;
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
log.component("premise-core").warn("premise-core not available, using fallback scanner", {
|
|
2036
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2037
|
+
});
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
async function loadSymbolIndex(projectDir) {
|
|
2042
|
+
log.flow("load-symbols").info("Loading symbols", { projectDir });
|
|
2043
|
+
const indexPath = path3.join(projectDir, ".paradigm", "index.json");
|
|
2044
|
+
if (fs3.existsSync(indexPath)) {
|
|
2045
|
+
try {
|
|
2046
|
+
log.component("index-loader").info("Found cached index", { path: indexPath });
|
|
2047
|
+
const content = fs3.readFileSync(indexPath, "utf-8");
|
|
2048
|
+
const index = JSON.parse(content);
|
|
2049
|
+
const entries = Array.isArray(index.entries) ? index.entries : Array.isArray(index) ? index : null;
|
|
2050
|
+
if (entries) {
|
|
2051
|
+
log.flow("load-symbols").info("Loaded from cached index", { count: entries.length });
|
|
2052
|
+
return entries;
|
|
2053
|
+
}
|
|
2054
|
+
} catch (error) {
|
|
2055
|
+
log.component("index-loader").error("Failed to load cached index", { error: String(error) });
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const premiseResult = await loadWithPremiseCore(projectDir);
|
|
2059
|
+
if (premiseResult) {
|
|
2060
|
+
return premiseResult;
|
|
2061
|
+
}
|
|
2062
|
+
log.flow("load-symbols").info("Using fallback scanner");
|
|
2063
|
+
return scanPurposeFiles(projectDir);
|
|
2064
|
+
}
|
|
2065
|
+
async function scanPurposeFiles(projectDir) {
|
|
2066
|
+
const symbols = [];
|
|
2067
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2068
|
+
const scanDirs = ["src", "lib", "packages", "apps", "."];
|
|
2069
|
+
for (const dir of scanDirs) {
|
|
2070
|
+
const fullPath = path3.join(projectDir, dir);
|
|
2071
|
+
if (fs3.existsSync(fullPath)) {
|
|
2072
|
+
await scanDirectory(fullPath, symbols, seenIds, projectDir);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const portalPath = path3.join(projectDir, "portal.yaml");
|
|
2076
|
+
if (fs3.existsSync(portalPath)) {
|
|
2077
|
+
log.component("gate-loader").debug("Found portal.yaml", { path: "portal.yaml" });
|
|
2078
|
+
try {
|
|
2079
|
+
const content = fs3.readFileSync(portalPath, "utf-8");
|
|
2080
|
+
const gatesSection = content.match(/^gates:\s*\n((?: .+\n)*)/m);
|
|
2081
|
+
if (gatesSection) {
|
|
2082
|
+
const gateMatches = gatesSection[1].matchAll(/^ ([a-z][a-z0-9-]*):/gm);
|
|
2083
|
+
for (const match of gateMatches) {
|
|
2084
|
+
const gateName = match[1];
|
|
2085
|
+
const id = `gate-${gateName}`;
|
|
2086
|
+
if (!seenIds.has(id)) {
|
|
2087
|
+
seenIds.add(id);
|
|
2088
|
+
symbols.push({
|
|
2089
|
+
id,
|
|
2090
|
+
symbol: `^${gateName}`,
|
|
2091
|
+
type: "gate",
|
|
2092
|
+
source: "portal",
|
|
2093
|
+
filePath: "portal.yaml",
|
|
2094
|
+
data: {},
|
|
2095
|
+
references: [],
|
|
2096
|
+
referencedBy: []
|
|
2097
|
+
});
|
|
2098
|
+
log.component("gate-loader").debug("Extracted gate", { symbol: `^${gateName}` });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
log.component("gate-loader").error("Failed to parse portal.yaml", { error: String(error) });
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
log.flow("load-symbols").info("Fallback scan complete", { count: symbols.length });
|
|
2107
|
+
return symbols;
|
|
2108
|
+
}
|
|
2109
|
+
async function scanDirectory(dir, symbols, seenIds, projectDir) {
|
|
2110
|
+
const skipDirs = ["node_modules", ".git", "dist", "build", ".paradigm", "coverage", ".next", ".svelte-kit"];
|
|
2111
|
+
let entries;
|
|
2112
|
+
try {
|
|
2113
|
+
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
2114
|
+
} catch {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
for (const entry of entries) {
|
|
2118
|
+
const fullPath = path3.join(dir, entry.name);
|
|
2119
|
+
if (entry.isDirectory()) {
|
|
2120
|
+
if (!skipDirs.includes(entry.name)) {
|
|
2121
|
+
await scanDirectory(fullPath, symbols, seenIds, projectDir);
|
|
2122
|
+
}
|
|
2123
|
+
} else if (entry.name === ".purpose") {
|
|
2124
|
+
const relativePath = path3.relative(projectDir, fullPath);
|
|
2125
|
+
log.component("purpose-loader").debug("Scanning .purpose file", { path: relativePath });
|
|
2126
|
+
try {
|
|
2127
|
+
const content = fs3.readFileSync(fullPath, "utf-8");
|
|
2128
|
+
const parsed = parsePurposeFile(content, fullPath, projectDir);
|
|
2129
|
+
for (const symbol of parsed) {
|
|
2130
|
+
if (!seenIds.has(symbol.id)) {
|
|
2131
|
+
seenIds.add(symbol.id);
|
|
2132
|
+
symbols.push(symbol);
|
|
2133
|
+
log.component("purpose-loader").debug("Extracted symbol", {
|
|
2134
|
+
symbol: symbol.symbol,
|
|
2135
|
+
type: symbol.type,
|
|
2136
|
+
file: relativePath
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
log.component("purpose-loader").error("Failed to parse .purpose file", {
|
|
2142
|
+
path: relativePath,
|
|
2143
|
+
error: String(error)
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function parsePurposeFile(content, filePath, projectDir) {
|
|
2150
|
+
const symbols = [];
|
|
2151
|
+
const relativePath = path3.relative(projectDir, filePath);
|
|
2152
|
+
const componentMatches = content.matchAll(/(?:^|\s)#([a-z][a-z0-9-]*)/gm);
|
|
2153
|
+
for (const match of componentMatches) {
|
|
2154
|
+
const name = match[1];
|
|
2155
|
+
symbols.push({
|
|
2156
|
+
id: `component-${name}`,
|
|
2157
|
+
symbol: `#${name}`,
|
|
2158
|
+
type: "component",
|
|
2159
|
+
source: "purpose",
|
|
2160
|
+
filePath: relativePath,
|
|
2161
|
+
data: {},
|
|
2162
|
+
description: extractDescription(content, `#${name}`),
|
|
2163
|
+
references: extractReferences(content),
|
|
2164
|
+
referencedBy: [],
|
|
2165
|
+
tags: extractTags(content)
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
const flowMatches = content.matchAll(/\$([a-z][a-z0-9-]*)/gm);
|
|
2169
|
+
for (const match of flowMatches) {
|
|
2170
|
+
const name = match[1];
|
|
2171
|
+
const symbol = `$${name}`;
|
|
2172
|
+
if (SYMBOL_BLOCKLIST.has(symbol)) {
|
|
2173
|
+
log.component("purpose-loader").debug("Skipping blocklisted symbol", { symbol });
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
if (!symbols.find((s) => s.symbol === symbol)) {
|
|
2177
|
+
symbols.push({
|
|
2178
|
+
id: `flow-${name}`,
|
|
2179
|
+
symbol,
|
|
2180
|
+
type: "flow",
|
|
2181
|
+
source: "purpose",
|
|
2182
|
+
filePath: relativePath,
|
|
2183
|
+
data: {},
|
|
2184
|
+
references: [],
|
|
2185
|
+
referencedBy: []
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
const signalMatches = content.matchAll(/!([a-z][a-z0-9-]*)/gm);
|
|
2190
|
+
for (const match of signalMatches) {
|
|
2191
|
+
const name = match[1];
|
|
2192
|
+
if (!symbols.find((s) => s.symbol === `!${name}`)) {
|
|
2193
|
+
symbols.push({
|
|
2194
|
+
id: `signal-${name}`,
|
|
2195
|
+
symbol: `!${name}`,
|
|
2196
|
+
type: "signal",
|
|
2197
|
+
source: "purpose",
|
|
2198
|
+
filePath: relativePath,
|
|
2199
|
+
data: {},
|
|
2200
|
+
references: [],
|
|
2201
|
+
referencedBy: []
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
const gateMatches = content.matchAll(/\^([a-z][a-z0-9-]*)/gm);
|
|
2206
|
+
for (const match of gateMatches) {
|
|
2207
|
+
const name = match[1];
|
|
2208
|
+
if (!symbols.find((s) => s.symbol === `^${name}`)) {
|
|
2209
|
+
symbols.push({
|
|
2210
|
+
id: `gate-${name}`,
|
|
2211
|
+
symbol: `^${name}`,
|
|
2212
|
+
type: "gate",
|
|
2213
|
+
source: "purpose",
|
|
2214
|
+
filePath: relativePath,
|
|
2215
|
+
data: {},
|
|
2216
|
+
references: [],
|
|
2217
|
+
referencedBy: []
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
const aspectMatches = content.matchAll(/~([a-z][a-z0-9-]*)/gm);
|
|
2222
|
+
for (const match of aspectMatches) {
|
|
2223
|
+
const name = match[1];
|
|
2224
|
+
if (!symbols.find((s) => s.symbol === `~${name}`)) {
|
|
2225
|
+
symbols.push({
|
|
2226
|
+
id: `aspect-${name}`,
|
|
2227
|
+
symbol: `~${name}`,
|
|
2228
|
+
type: "aspect",
|
|
2229
|
+
source: "purpose",
|
|
2230
|
+
filePath: relativePath,
|
|
2231
|
+
data: {},
|
|
2232
|
+
references: [],
|
|
2233
|
+
referencedBy: []
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return symbols;
|
|
2238
|
+
}
|
|
2239
|
+
function extractDescription(content, symbol) {
|
|
2240
|
+
const regex = new RegExp(`${symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[-:]?\\s*(.+)`, "m");
|
|
2241
|
+
const match = content.match(regex);
|
|
2242
|
+
if (match && match[1]) {
|
|
2243
|
+
return match[1].trim();
|
|
2244
|
+
}
|
|
2245
|
+
return void 0;
|
|
2246
|
+
}
|
|
2247
|
+
function extractReferences(content) {
|
|
2248
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2249
|
+
const refMatches = content.matchAll(/[@#$!^~]([a-z][a-z0-9-]*)/g);
|
|
2250
|
+
for (const match of refMatches) {
|
|
2251
|
+
const symbol = match[0];
|
|
2252
|
+
if (!SYMBOL_BLOCKLIST.has(symbol)) {
|
|
2253
|
+
refs.add(symbol);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return Array.from(refs);
|
|
2257
|
+
}
|
|
2258
|
+
function extractTags(content) {
|
|
2259
|
+
const tagMatch = content.match(/tags:\s*\[([^\]]+)\]/);
|
|
2260
|
+
if (tagMatch) {
|
|
2261
|
+
return tagMatch[1].split(",").map((t) => t.trim().replace(/^["']|["']$/g, ""));
|
|
2262
|
+
}
|
|
2263
|
+
return [];
|
|
2264
|
+
}
|
|
2265
|
+
async function getSymbolCount(projectDir) {
|
|
2266
|
+
const symbols = await loadSymbolIndex(projectDir);
|
|
2267
|
+
return symbols.length;
|
|
2268
|
+
}
|
|
2269
|
+
async function updateSymbol(projectDir, symbolId, updates) {
|
|
2270
|
+
const symbols = await loadSymbolIndex(projectDir);
|
|
2271
|
+
const symbol = symbols.find((s) => s.id === symbolId);
|
|
2272
|
+
if (!symbol) {
|
|
2273
|
+
return { success: false, error: "Symbol not found" };
|
|
2274
|
+
}
|
|
2275
|
+
const filePath = path3.join(projectDir, symbol.filePath);
|
|
2276
|
+
if (!fs3.existsSync(filePath)) {
|
|
2277
|
+
return { success: false, error: "Source file not found" };
|
|
2278
|
+
}
|
|
2279
|
+
try {
|
|
2280
|
+
let content = fs3.readFileSync(filePath, "utf-8");
|
|
2281
|
+
let modified = false;
|
|
2282
|
+
if (updates.description !== void 0) {
|
|
2283
|
+
const symbolPattern = symbol.symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2284
|
+
const descRegex = new RegExp(`(${symbolPattern})\\s*[-:]?\\s*(.*)`, "m");
|
|
2285
|
+
const match = content.match(descRegex);
|
|
2286
|
+
if (match) {
|
|
2287
|
+
const newLine = updates.description ? `${symbol.symbol}: ${updates.description}` : symbol.symbol;
|
|
2288
|
+
content = content.replace(descRegex, newLine);
|
|
2289
|
+
modified = true;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
if (updates.tags !== void 0) {
|
|
2293
|
+
const tagsStr = updates.tags.length > 0 ? `tags: [${updates.tags.map((t) => `"${t}"`).join(", ")}]` : "";
|
|
2294
|
+
const tagsRegex = /^tags:\s*\[[^\]]*\]\s*$/m;
|
|
2295
|
+
if (tagsRegex.test(content)) {
|
|
2296
|
+
if (tagsStr) {
|
|
2297
|
+
content = content.replace(tagsRegex, tagsStr);
|
|
2298
|
+
} else {
|
|
2299
|
+
content = content.replace(tagsRegex, "");
|
|
2300
|
+
}
|
|
2301
|
+
modified = true;
|
|
2302
|
+
} else if (tagsStr) {
|
|
2303
|
+
const symbolPattern = symbol.symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2304
|
+
const symbolLineRegex = new RegExp(`(${symbolPattern}[^\\n]*\\n)`, "m");
|
|
2305
|
+
const symbolMatch = content.match(symbolLineRegex);
|
|
2306
|
+
if (symbolMatch) {
|
|
2307
|
+
content = content.replace(symbolLineRegex, `$1${tagsStr}
|
|
2308
|
+
`);
|
|
2309
|
+
modified = true;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (modified) {
|
|
2314
|
+
content = content.replace(/\n{3,}/g, "\n\n");
|
|
2315
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
2316
|
+
log.component("symbol-updater").info("Updated symbol", { symbol: symbol.symbol, file: symbol.filePath });
|
|
2317
|
+
const indexPath = path3.join(projectDir, ".paradigm", "index.json");
|
|
2318
|
+
if (fs3.existsSync(indexPath)) {
|
|
2319
|
+
try {
|
|
2320
|
+
const indexContent = fs3.readFileSync(indexPath, "utf-8");
|
|
2321
|
+
const index = JSON.parse(indexContent);
|
|
2322
|
+
const entries = Array.isArray(index.entries) ? index.entries : index;
|
|
2323
|
+
const entryIndex = entries.findIndex((e) => e.id === symbolId);
|
|
2324
|
+
if (entryIndex >= 0) {
|
|
2325
|
+
if (updates.description !== void 0) {
|
|
2326
|
+
entries[entryIndex].description = updates.description;
|
|
2327
|
+
}
|
|
2328
|
+
if (updates.tags !== void 0) {
|
|
2329
|
+
entries[entryIndex].tags = updates.tags;
|
|
2330
|
+
}
|
|
2331
|
+
if (Array.isArray(index.entries)) {
|
|
2332
|
+
index.entries = entries;
|
|
2333
|
+
fs3.writeFileSync(indexPath, JSON.stringify(index, null, 2), "utf-8");
|
|
2334
|
+
} else {
|
|
2335
|
+
fs3.writeFileSync(indexPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
} catch {
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
return { success: true };
|
|
2342
|
+
}
|
|
2343
|
+
return { success: true };
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
log.component("symbol-updater").error("Failed to update symbol", { error: String(error) });
|
|
2346
|
+
return { success: false, error: "Failed to write file" };
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/server/routes/symbols.ts
|
|
2351
|
+
var LOG_LEVEL2 = process.env.SENTINEL_LOG_LEVEL || process.env.LOG_LEVEL || "info";
|
|
2352
|
+
var shouldLog2 = (level) => {
|
|
2353
|
+
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
2354
|
+
return levels[level] >= levels[LOG_LEVEL2];
|
|
2355
|
+
};
|
|
2356
|
+
var log2 = {
|
|
2357
|
+
gate(name) {
|
|
2358
|
+
const symbol = chalk2.cyan(`^${name}`);
|
|
2359
|
+
return {
|
|
2360
|
+
info: (msg, data) => {
|
|
2361
|
+
if (shouldLog2("info")) {
|
|
2362
|
+
const dataStr = data ? chalk2.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
|
|
2363
|
+
console.log(`${chalk2.blue("\u2139")} ${symbol} ${msg}${dataStr}`);
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
error: (msg, data) => {
|
|
2367
|
+
if (shouldLog2("error")) {
|
|
2368
|
+
const dataStr = data ? chalk2.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
|
|
2369
|
+
console.error(`${chalk2.red("\u2716")} ${symbol} ${msg}${dataStr}`);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
function createSymbolsRouter(projectDir) {
|
|
2376
|
+
const router = Router();
|
|
2377
|
+
router.get("/", async (_req, res) => {
|
|
2378
|
+
try {
|
|
2379
|
+
const symbols = await loadSymbolIndex(projectDir);
|
|
2380
|
+
log2.gate("api-symbols").info("Symbols loaded", { count: symbols.length });
|
|
2381
|
+
res.json({ symbols });
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
log2.gate("api-symbols").error("Failed to load symbols", { error: String(error) });
|
|
2384
|
+
res.status(500).json({ error: "Failed to load symbols" });
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
router.put("/:id", async (req, res) => {
|
|
2388
|
+
try {
|
|
2389
|
+
const { id } = req.params;
|
|
2390
|
+
const updates = req.body;
|
|
2391
|
+
log2.gate("api-symbols").info("Update requested", { id, updates: JSON.stringify(updates) });
|
|
2392
|
+
if (updates.tags && !Array.isArray(updates.tags)) {
|
|
2393
|
+
res.status(400).json({ error: "Tags must be an array" });
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const result = await updateSymbol(projectDir, id, updates);
|
|
2397
|
+
if (result.success) {
|
|
2398
|
+
const symbols = await loadSymbolIndex(projectDir);
|
|
2399
|
+
const updatedSymbol = symbols.find((s) => s.id === id);
|
|
2400
|
+
log2.gate("api-symbols").info("Symbol updated", { id });
|
|
2401
|
+
res.json({ success: true, symbol: updatedSymbol });
|
|
2402
|
+
} else {
|
|
2403
|
+
log2.gate("api-symbols").error("Update failed", { id, error: result.error });
|
|
2404
|
+
res.status(400).json({ success: false, error: result.error });
|
|
2405
|
+
}
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
log2.gate("api-symbols").error("Failed to update symbol", { error: String(error) });
|
|
2408
|
+
res.status(500).json({ error: "Failed to update symbol" });
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
return router;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// src/server/routes/info.ts
|
|
2415
|
+
import { Router as Router2 } from "express";
|
|
2416
|
+
function createInfoRouter(projectDir) {
|
|
2417
|
+
const router = Router2();
|
|
2418
|
+
router.get("/", async (_req, res) => {
|
|
2419
|
+
try {
|
|
2420
|
+
const config = await loadParadigmConfig(projectDir);
|
|
2421
|
+
const symbolCount = await getSymbolCount(projectDir);
|
|
2422
|
+
res.json({
|
|
2423
|
+
projectName: config.name || null,
|
|
2424
|
+
discipline: config.discipline || null,
|
|
2425
|
+
symbolCount,
|
|
2426
|
+
projectDir
|
|
2427
|
+
});
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
console.error("Failed to load project info:", error);
|
|
2430
|
+
res.status(500).json({ error: "Failed to load project info" });
|
|
2431
|
+
}
|
|
2432
|
+
});
|
|
2433
|
+
return router;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// src/server/routes/commits.ts
|
|
2437
|
+
import { Router as Router3 } from "express";
|
|
2438
|
+
|
|
2439
|
+
// src/server/loaders/git.ts
|
|
2440
|
+
import simpleGit from "simple-git";
|
|
2441
|
+
import * as path4 from "path";
|
|
2442
|
+
function extractSymbolsFromFiles(files) {
|
|
2443
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
2444
|
+
for (const file of files) {
|
|
2445
|
+
if (file.endsWith(".purpose")) {
|
|
2446
|
+
const dir = path4.dirname(file);
|
|
2447
|
+
const name = path4.basename(dir);
|
|
2448
|
+
if (dir.includes("features/") || dir.includes("routes/") || dir.includes("api/")) {
|
|
2449
|
+
symbols.add(`@${name}`);
|
|
2450
|
+
} else if (dir.includes("components/") || dir.includes("lib/") || dir.includes("utils/")) {
|
|
2451
|
+
symbols.add(`#${name}`);
|
|
2452
|
+
} else if (dir.includes("middleware/") || dir.includes("auth/") || dir.includes("guards/")) {
|
|
2453
|
+
symbols.add(`^${name}`);
|
|
2454
|
+
} else if (dir.includes("flows/") || dir.includes("workflows/")) {
|
|
2455
|
+
symbols.add(`$${name}`);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
if (file.includes("portal.yaml")) {
|
|
2459
|
+
symbols.add("^portal");
|
|
2460
|
+
}
|
|
2461
|
+
const featureMatch = file.match(/features\/([^/]+)/);
|
|
2462
|
+
if (featureMatch) {
|
|
2463
|
+
symbols.add(`@${featureMatch[1]}`);
|
|
2464
|
+
}
|
|
2465
|
+
const componentMatch = file.match(/components\/([^/]+)/);
|
|
2466
|
+
if (componentMatch) {
|
|
2467
|
+
symbols.add(`#${componentMatch[1]}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return Array.from(symbols);
|
|
2471
|
+
}
|
|
2472
|
+
async function loadGitHistory(projectDir, options = {}) {
|
|
2473
|
+
const git = simpleGit(projectDir);
|
|
2474
|
+
const isRepo = await git.checkIsRepo();
|
|
2475
|
+
if (!isRepo) {
|
|
2476
|
+
return [];
|
|
2477
|
+
}
|
|
2478
|
+
try {
|
|
2479
|
+
const logOptions = {
|
|
2480
|
+
maxCount: options.limit || 100
|
|
2481
|
+
};
|
|
2482
|
+
if (options.since) {
|
|
2483
|
+
logOptions["--since"] = options.since;
|
|
2484
|
+
}
|
|
2485
|
+
const log4 = await git.log(logOptions);
|
|
2486
|
+
const commits = [];
|
|
2487
|
+
for (const commit of log4.all) {
|
|
2488
|
+
let filesChanged = [];
|
|
2489
|
+
let symbolsModified = [];
|
|
2490
|
+
try {
|
|
2491
|
+
const diff = await git.diffSummary([`${commit.hash}^`, commit.hash]);
|
|
2492
|
+
filesChanged = diff.files.map((f) => f.file);
|
|
2493
|
+
symbolsModified = extractSymbolsFromFiles(filesChanged);
|
|
2494
|
+
} catch {
|
|
2495
|
+
}
|
|
2496
|
+
commits.push({
|
|
2497
|
+
hash: commit.hash,
|
|
2498
|
+
shortHash: commit.hash.slice(0, 7),
|
|
2499
|
+
date: commit.date,
|
|
2500
|
+
author: commit.author_name,
|
|
2501
|
+
message: commit.message.split("\n")[0],
|
|
2502
|
+
// First line only
|
|
2503
|
+
symbolsModified,
|
|
2504
|
+
filesChanged
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
return commits;
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
console.error("Failed to load git history:", error);
|
|
2510
|
+
return [];
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/server/routes/commits.ts
|
|
2515
|
+
function createCommitsRouter(projectDir) {
|
|
2516
|
+
const router = Router3();
|
|
2517
|
+
router.get("/", async (req, res) => {
|
|
2518
|
+
try {
|
|
2519
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
2520
|
+
const since = req.query.since;
|
|
2521
|
+
const commits = await loadGitHistory(projectDir, { limit, since });
|
|
2522
|
+
res.json({ commits });
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
console.error("Failed to load commits:", error);
|
|
2525
|
+
res.status(500).json({ error: "Failed to load commits" });
|
|
2526
|
+
}
|
|
2527
|
+
});
|
|
2528
|
+
return router;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// src/server/routes/incidents.ts
|
|
2532
|
+
import { Router as Router4 } from "express";
|
|
2533
|
+
function createIncidentsRouter(_projectDir) {
|
|
2534
|
+
const router = Router4();
|
|
2535
|
+
const storage = new SentinelStorage();
|
|
2536
|
+
router.get("/", async (req, res) => {
|
|
2537
|
+
try {
|
|
2538
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
2539
|
+
const status = req.query.status;
|
|
2540
|
+
const environment = req.query.environment;
|
|
2541
|
+
const symbol = req.query.symbol;
|
|
2542
|
+
const options = { limit };
|
|
2543
|
+
if (status && ["open", "investigating", "resolved", "wont-fix"].includes(status)) {
|
|
2544
|
+
options.status = status;
|
|
2545
|
+
}
|
|
2546
|
+
if (environment) options.environment = environment;
|
|
2547
|
+
if (symbol) options.symbol = symbol;
|
|
2548
|
+
const incidents = storage.getRecentIncidents(options);
|
|
2549
|
+
const summaries = incidents.map((incident) => ({
|
|
2550
|
+
id: incident.id,
|
|
2551
|
+
timestamp: incident.timestamp,
|
|
2552
|
+
status: incident.status,
|
|
2553
|
+
error: {
|
|
2554
|
+
message: incident.error.message,
|
|
2555
|
+
type: incident.error.type
|
|
2556
|
+
},
|
|
2557
|
+
symbols: incident.symbols,
|
|
2558
|
+
environment: incident.environment,
|
|
2559
|
+
patternMatches: []
|
|
2560
|
+
// Would need PatternMatcher to populate
|
|
2561
|
+
}));
|
|
2562
|
+
res.json({ incidents: summaries });
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
console.error("Failed to load incidents:", error);
|
|
2565
|
+
res.status(500).json({ error: "Failed to load incidents" });
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
router.get("/:id", async (req, res) => {
|
|
2569
|
+
try {
|
|
2570
|
+
const incident = storage.getIncident(req.params.id);
|
|
2571
|
+
if (!incident) {
|
|
2572
|
+
res.status(404).json({ error: "Incident not found" });
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
res.json({ incident });
|
|
2576
|
+
} catch (error) {
|
|
2577
|
+
console.error("Failed to load incident:", error);
|
|
2578
|
+
res.status(500).json({ error: "Failed to load incident" });
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
router.post("/:id/resolve", async (req, res) => {
|
|
2582
|
+
try {
|
|
2583
|
+
const incident = storage.getIncident(req.params.id);
|
|
2584
|
+
if (!incident) {
|
|
2585
|
+
res.status(404).json({ error: "Incident not found" });
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
storage.resolveIncident(req.params.id, {
|
|
2589
|
+
notes: req.body.notes,
|
|
2590
|
+
patternId: req.body.patternId
|
|
2591
|
+
});
|
|
2592
|
+
res.json({ success: true });
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
console.error("Failed to resolve incident:", error);
|
|
2595
|
+
res.status(500).json({ error: "Failed to resolve incident" });
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
return router;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// src/server/routes/patterns.ts
|
|
2602
|
+
import { Router as Router5 } from "express";
|
|
2603
|
+
function createPatternsRouter(_projectDir) {
|
|
2604
|
+
const router = Router5();
|
|
2605
|
+
const storage = new SentinelStorage();
|
|
2606
|
+
router.get("/", async (req, res) => {
|
|
2607
|
+
try {
|
|
2608
|
+
const source = req.query.source;
|
|
2609
|
+
const symbol = req.query.symbol;
|
|
2610
|
+
const minConfidence = parseInt(req.query.minConfidence) || void 0;
|
|
2611
|
+
const options = {};
|
|
2612
|
+
if (source && ["manual", "suggested", "imported", "community"].includes(source)) {
|
|
2613
|
+
options.source = source;
|
|
2614
|
+
}
|
|
2615
|
+
if (symbol) options.symbol = symbol;
|
|
2616
|
+
if (minConfidence) options.minConfidence = minConfidence;
|
|
2617
|
+
const patterns = storage.getAllPatterns(options);
|
|
2618
|
+
const summaries = patterns.map((pattern) => ({
|
|
2619
|
+
id: pattern.id,
|
|
2620
|
+
name: pattern.name,
|
|
2621
|
+
description: pattern.description,
|
|
2622
|
+
confidence: {
|
|
2623
|
+
score: pattern.confidence.score,
|
|
2624
|
+
timesMatched: pattern.confidence.timesMatched,
|
|
2625
|
+
timesResolved: pattern.confidence.timesResolved
|
|
2626
|
+
},
|
|
2627
|
+
tags: pattern.tags
|
|
2628
|
+
}));
|
|
2629
|
+
res.json({ patterns: summaries });
|
|
2630
|
+
} catch (error) {
|
|
2631
|
+
console.error("Failed to load patterns:", error);
|
|
2632
|
+
res.status(500).json({ error: "Failed to load patterns" });
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
router.get("/:id", async (req, res) => {
|
|
2636
|
+
try {
|
|
2637
|
+
const pattern = storage.getPattern(req.params.id);
|
|
2638
|
+
if (!pattern) {
|
|
2639
|
+
res.status(404).json({ error: "Pattern not found" });
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
res.json({ pattern });
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
console.error("Failed to load pattern:", error);
|
|
2645
|
+
res.status(500).json({ error: "Failed to load pattern" });
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
return router;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/server/index.ts
|
|
2652
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2653
|
+
var __dirname2 = path5.dirname(__filename2);
|
|
2654
|
+
var log3 = {
|
|
2655
|
+
component(name) {
|
|
2656
|
+
const symbol = chalk3.magenta(`#${name}`);
|
|
2657
|
+
return {
|
|
2658
|
+
info: (msg, data) => {
|
|
2659
|
+
const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
|
|
2660
|
+
console.log(`${chalk3.blue("\u2139")} ${symbol} ${msg}${dataStr}`);
|
|
2661
|
+
},
|
|
2662
|
+
success: (msg, data) => {
|
|
2663
|
+
const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
|
|
2664
|
+
console.log(`${chalk3.green("\u2714")} ${symbol} ${msg}${dataStr}`);
|
|
2665
|
+
},
|
|
2666
|
+
warn: (msg, data) => {
|
|
2667
|
+
const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
|
|
2668
|
+
console.log(`${chalk3.yellow("\u26A0")} ${symbol} ${msg}${dataStr}`);
|
|
2669
|
+
},
|
|
2670
|
+
error: (msg, data) => {
|
|
2671
|
+
const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
|
|
2672
|
+
console.error(`${chalk3.red("\u2716")} ${symbol} ${msg}${dataStr}`);
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
function createApp(options) {
|
|
2678
|
+
const app = express();
|
|
2679
|
+
app.use(express.json());
|
|
2680
|
+
app.use((_req, res, next) => {
|
|
2681
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
2682
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
2683
|
+
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
2684
|
+
next();
|
|
2685
|
+
});
|
|
2686
|
+
app.use("/api/symbols", createSymbolsRouter(options.projectDir));
|
|
2687
|
+
app.use("/api/info", createInfoRouter(options.projectDir));
|
|
2688
|
+
app.use("/api/commits", createCommitsRouter(options.projectDir));
|
|
2689
|
+
app.use("/api/incidents", createIncidentsRouter(options.projectDir));
|
|
2690
|
+
app.use("/api/patterns", createPatternsRouter(options.projectDir));
|
|
2691
|
+
app.get("/api/health", (_req, res) => {
|
|
2692
|
+
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2693
|
+
});
|
|
2694
|
+
const uiDistPath = path5.join(__dirname2, "..", "..", "ui", "dist");
|
|
2695
|
+
if (fs4.existsSync(uiDistPath)) {
|
|
2696
|
+
app.use(express.static(uiDistPath));
|
|
2697
|
+
app.get("{*path}", (req, res) => {
|
|
2698
|
+
if (!req.path.startsWith("/api")) {
|
|
2699
|
+
res.sendFile(path5.join(uiDistPath, "index.html"));
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
return app;
|
|
2704
|
+
}
|
|
2705
|
+
async function startServer(options) {
|
|
2706
|
+
const app = createApp(options);
|
|
2707
|
+
log3.component("sentinel-server").info("Starting server", { port: options.port });
|
|
2708
|
+
log3.component("sentinel-server").info("Project directory", { path: options.projectDir });
|
|
2709
|
+
return new Promise((resolve, reject) => {
|
|
2710
|
+
const server = app.listen(options.port, () => {
|
|
2711
|
+
log3.component("sentinel-server").success("Server running", { url: `http://localhost:${options.port}` });
|
|
2712
|
+
if (options.open) {
|
|
2713
|
+
import("open").then((openModule) => {
|
|
2714
|
+
openModule.default(`http://localhost:${options.port}`);
|
|
2715
|
+
log3.component("sentinel-server").info("Opened browser");
|
|
2716
|
+
}).catch(() => {
|
|
2717
|
+
log3.component("sentinel-server").warn("Could not open browser automatically");
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
resolve();
|
|
2721
|
+
});
|
|
2722
|
+
server.on("error", (err) => {
|
|
2723
|
+
if (err.code === "EADDRINUSE") {
|
|
2724
|
+
log3.component("sentinel-server").error("Port already in use", { port: options.port });
|
|
2725
|
+
} else {
|
|
2726
|
+
log3.component("sentinel-server").error("Server error", { error: err.message });
|
|
2727
|
+
}
|
|
2728
|
+
reject(err);
|
|
2729
|
+
});
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// src/config.ts
|
|
2734
|
+
import * as fs5 from "fs";
|
|
2735
|
+
import * as path6 from "path";
|
|
2736
|
+
function writeConfig(projectDir, config) {
|
|
2737
|
+
const filePath = path6.join(projectDir, ".sentinel.yaml");
|
|
2738
|
+
const content = serializeSimpleYaml(config);
|
|
2739
|
+
fs5.writeFileSync(filePath, content, "utf-8");
|
|
2740
|
+
}
|
|
2741
|
+
function serializeSimpleYaml(config) {
|
|
2742
|
+
const lines = [];
|
|
2743
|
+
lines.push(`# Sentinel Configuration`);
|
|
2744
|
+
lines.push(`# Auto-generated \u2014 edit freely`);
|
|
2745
|
+
lines.push("");
|
|
2746
|
+
lines.push(`version: "${config.version}"`);
|
|
2747
|
+
lines.push(`project: "${config.project}"`);
|
|
2748
|
+
if (config.environment) {
|
|
2749
|
+
lines.push(`environment: "${config.environment}"`);
|
|
2750
|
+
}
|
|
2751
|
+
if (config.symbols) {
|
|
2752
|
+
lines.push("");
|
|
2753
|
+
lines.push("symbols:");
|
|
2754
|
+
for (const [key, values] of Object.entries(config.symbols)) {
|
|
2755
|
+
if (values && values.length > 0) {
|
|
2756
|
+
lines.push(` ${key}:`);
|
|
2757
|
+
for (const v of values) {
|
|
2758
|
+
lines.push(` - ${v}`);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
if (config.routes && Object.keys(config.routes).length > 0) {
|
|
2764
|
+
lines.push("");
|
|
2765
|
+
lines.push("routes:");
|
|
2766
|
+
for (const [route, symbol] of Object.entries(config.routes)) {
|
|
2767
|
+
lines.push(` "${route}": ${symbol}`);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
if (config.scrub) {
|
|
2771
|
+
lines.push("");
|
|
2772
|
+
lines.push("scrub:");
|
|
2773
|
+
if (config.scrub.headers?.length) {
|
|
2774
|
+
lines.push(" headers:");
|
|
2775
|
+
for (const h of config.scrub.headers) {
|
|
2776
|
+
lines.push(` - ${h}`);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (config.scrub.fields?.length) {
|
|
2780
|
+
lines.push(" fields:");
|
|
2781
|
+
for (const f of config.scrub.fields) {
|
|
2782
|
+
lines.push(` - ${f}`);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
lines.push("");
|
|
2787
|
+
return lines.join("\n");
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/detector.ts
|
|
2791
|
+
import * as fs6 from "fs";
|
|
2792
|
+
import * as path7 from "path";
|
|
2793
|
+
var DIR_PATTERNS = [
|
|
2794
|
+
{ dirs: ["services", "src/services"], prefix: "#", type: "components" },
|
|
2795
|
+
{ dirs: ["routes", "src/routes", "api", "src/api"], prefix: "#", type: "components" },
|
|
2796
|
+
{ dirs: ["handlers", "src/handlers"], prefix: "#", type: "components" },
|
|
2797
|
+
{ dirs: ["controllers", "src/controllers"], prefix: "#", type: "components" },
|
|
2798
|
+
{ dirs: ["components", "src/components"], prefix: "#", type: "components" },
|
|
2799
|
+
{ dirs: ["lib", "src/lib"], prefix: "#", type: "components" },
|
|
2800
|
+
{ dirs: ["middleware", "src/middleware"], prefix: "^", type: "gates" },
|
|
2801
|
+
{ dirs: ["guards", "src/guards"], prefix: "^", type: "gates" },
|
|
2802
|
+
{ dirs: ["auth", "src/auth"], prefix: "^", type: "gates" },
|
|
2803
|
+
{ dirs: ["events", "src/events"], prefix: "!", type: "signals" },
|
|
2804
|
+
{ dirs: ["listeners", "src/listeners"], prefix: "!", type: "signals" },
|
|
2805
|
+
{ dirs: ["flows", "src/flows"], prefix: "$", type: "flows" },
|
|
2806
|
+
{ dirs: ["workflows", "src/workflows"], prefix: "$", type: "flows" },
|
|
2807
|
+
{ dirs: ["pipelines", "src/pipelines"], prefix: "$", type: "flows" }
|
|
2808
|
+
];
|
|
2809
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx", ".mjs", ".mts"]);
|
|
2810
|
+
function detectSymbols(projectDir) {
|
|
2811
|
+
const result = {
|
|
2812
|
+
components: [],
|
|
2813
|
+
gates: [],
|
|
2814
|
+
flows: [],
|
|
2815
|
+
signals: [],
|
|
2816
|
+
routes: {}
|
|
2817
|
+
};
|
|
2818
|
+
const purposeSymbols = readPurposeFiles(projectDir);
|
|
2819
|
+
if (purposeSymbols) {
|
|
2820
|
+
result.components.push(...purposeSymbols.components);
|
|
2821
|
+
result.gates.push(...purposeSymbols.gates);
|
|
2822
|
+
result.flows.push(...purposeSymbols.flows);
|
|
2823
|
+
result.signals.push(...purposeSymbols.signals);
|
|
2824
|
+
}
|
|
2825
|
+
for (const pattern of DIR_PATTERNS) {
|
|
2826
|
+
for (const dir of pattern.dirs) {
|
|
2827
|
+
const fullPath = path7.join(projectDir, dir);
|
|
2828
|
+
if (!fs6.existsSync(fullPath)) continue;
|
|
2829
|
+
const files = safeReaddir(fullPath);
|
|
2830
|
+
for (const file of files) {
|
|
2831
|
+
const ext = path7.extname(file);
|
|
2832
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
2833
|
+
const name = path7.basename(file, ext);
|
|
2834
|
+
if (name === "index" || name.endsWith(".test") || name.endsWith(".spec")) continue;
|
|
2835
|
+
const symbol = `${pattern.prefix}${toKebabCase(name)}`;
|
|
2836
|
+
if (!result[pattern.type].includes(symbol)) {
|
|
2837
|
+
result[pattern.type].push(symbol);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
scanRoutes(projectDir, result);
|
|
2843
|
+
return result;
|
|
2844
|
+
}
|
|
2845
|
+
function generateConfig(projectDir) {
|
|
2846
|
+
const detected = detectSymbols(projectDir);
|
|
2847
|
+
return {
|
|
2848
|
+
version: "1.0",
|
|
2849
|
+
project: path7.basename(projectDir),
|
|
2850
|
+
symbols: {
|
|
2851
|
+
components: detected.components.length > 0 ? detected.components : void 0,
|
|
2852
|
+
gates: detected.gates.length > 0 ? detected.gates : void 0,
|
|
2853
|
+
flows: detected.flows.length > 0 ? detected.flows : void 0,
|
|
2854
|
+
signals: detected.signals.length > 0 ? detected.signals : void 0
|
|
2855
|
+
},
|
|
2856
|
+
routes: Object.keys(detected.routes).length > 0 ? detected.routes : void 0
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
function readPurposeFiles(projectDir) {
|
|
2860
|
+
const paradigmDir = path7.join(projectDir, ".paradigm");
|
|
2861
|
+
if (!fs6.existsSync(paradigmDir)) return null;
|
|
2862
|
+
const result = {
|
|
2863
|
+
components: [],
|
|
2864
|
+
gates: [],
|
|
2865
|
+
flows: [],
|
|
2866
|
+
signals: [],
|
|
2867
|
+
routes: {}
|
|
2868
|
+
};
|
|
2869
|
+
const purposeFiles = findFiles(projectDir, ".purpose");
|
|
2870
|
+
for (const file of purposeFiles) {
|
|
2871
|
+
try {
|
|
2872
|
+
const content = fs6.readFileSync(file, "utf-8");
|
|
2873
|
+
extractPurposeSymbols(content, result);
|
|
2874
|
+
} catch {
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
const hasAny = result.components.length > 0 || result.gates.length > 0 || result.flows.length > 0 || result.signals.length > 0;
|
|
2878
|
+
return hasAny ? result : null;
|
|
2879
|
+
}
|
|
2880
|
+
function extractPurposeSymbols(content, result) {
|
|
2881
|
+
const lines = content.split("\n");
|
|
2882
|
+
let currentSection = "";
|
|
2883
|
+
for (const line of lines) {
|
|
2884
|
+
const trimmed = line.trim();
|
|
2885
|
+
if (trimmed === "components:" || trimmed === "features:") {
|
|
2886
|
+
currentSection = "components";
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
if (trimmed === "gates:") {
|
|
2890
|
+
currentSection = "gates";
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
if (trimmed === "flows:") {
|
|
2894
|
+
currentSection = "flows";
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
if (trimmed === "signals:") {
|
|
2898
|
+
currentSection = "signals";
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
if (currentSection && /^\s{2}\S/.test(line)) {
|
|
2902
|
+
const idMatch = trimmed.match(/^([a-zA-Z][\w-]*):$/);
|
|
2903
|
+
if (idMatch) {
|
|
2904
|
+
const prefixes = {
|
|
2905
|
+
components: "#",
|
|
2906
|
+
gates: "^",
|
|
2907
|
+
flows: "$",
|
|
2908
|
+
signals: "!"
|
|
2909
|
+
};
|
|
2910
|
+
const prefix = prefixes[currentSection] || "#";
|
|
2911
|
+
const symbol = `${prefix}${idMatch[1]}`;
|
|
2912
|
+
if (!result[currentSection]?.includes(symbol)) {
|
|
2913
|
+
result[currentSection]?.push(symbol);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
if (trimmed && !line.startsWith(" ") && !trimmed.endsWith(":")) {
|
|
2918
|
+
currentSection = "";
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
function scanRoutes(projectDir, result) {
|
|
2923
|
+
const routeDirs = ["routes", "src/routes", "api", "src/api"];
|
|
2924
|
+
for (const dir of routeDirs) {
|
|
2925
|
+
const fullPath = path7.join(projectDir, dir);
|
|
2926
|
+
if (!fs6.existsSync(fullPath)) continue;
|
|
2927
|
+
const files = safeReaddir(fullPath);
|
|
2928
|
+
for (const file of files) {
|
|
2929
|
+
const ext = path7.extname(file);
|
|
2930
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
2931
|
+
const name = path7.basename(file, ext);
|
|
2932
|
+
if (name === "index") continue;
|
|
2933
|
+
const routePrefix = `/api/${toKebabCase(name)}`;
|
|
2934
|
+
const component = `#${toKebabCase(name)}`;
|
|
2935
|
+
result.routes[routePrefix] = component;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
function toKebabCase(str) {
|
|
2940
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/\..*$/, "").toLowerCase();
|
|
2941
|
+
}
|
|
2942
|
+
function safeReaddir(dir) {
|
|
2943
|
+
try {
|
|
2944
|
+
return fs6.readdirSync(dir).filter((f) => {
|
|
2945
|
+
const fullPath = path7.join(dir, f);
|
|
2946
|
+
try {
|
|
2947
|
+
return fs6.statSync(fullPath).isFile();
|
|
2948
|
+
} catch {
|
|
2949
|
+
return false;
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
} catch {
|
|
2953
|
+
return [];
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
function findFiles(dir, filename, maxDepth = 4, depth = 0) {
|
|
2957
|
+
if (depth > maxDepth) return [];
|
|
2958
|
+
const results = [];
|
|
2959
|
+
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", "coverage", ".next", ".nuxt"]);
|
|
2960
|
+
try {
|
|
2961
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
2962
|
+
for (const entry of entries) {
|
|
2963
|
+
if (entry.isFile() && entry.name === filename) {
|
|
2964
|
+
results.push(path7.join(dir, entry.name));
|
|
2965
|
+
} else if (entry.isDirectory() && !skipDirs.has(entry.name)) {
|
|
2966
|
+
results.push(...findFiles(path7.join(dir, entry.name), filename, maxDepth, depth + 1));
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
} catch {
|
|
2970
|
+
}
|
|
2971
|
+
return results;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
// src/cli/format.ts
|
|
2975
|
+
import chalk4 from "chalk";
|
|
2976
|
+
function formatHeader() {
|
|
2977
|
+
return `
|
|
2978
|
+
${chalk4.cyan("\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\u2550\u2550\u2557")}
|
|
2979
|
+
${chalk4.cyan("\u2551")} ${chalk4.bold.white("SENTINEL TRIAGE")} ${chalk4.cyan("\u2551")}
|
|
2980
|
+
${chalk4.cyan("\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\u2550\u2550\u255D")}
|
|
2981
|
+
`;
|
|
2982
|
+
}
|
|
2983
|
+
function formatSummaryBar(stats) {
|
|
2984
|
+
return `${chalk4.cyan("\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\u2550\u2550\u2557")}
|
|
2985
|
+
${chalk4.cyan("\u2551")} Open: ${chalk4.yellow(String(stats.open).padEnd(4))} \u2502 Investigating: ${chalk4.blue(String(stats.investigating).padEnd(3))} \u2502 Resolved: ${chalk4.green(String(stats.resolved).padEnd(4))} \u2502 Today: ${chalk4.magenta(`+${stats.today}`)} ${chalk4.cyan("\u2551")}
|
|
2986
|
+
${chalk4.cyan("\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\u2550\u2550\u255D")}
|
|
2987
|
+
`;
|
|
2988
|
+
}
|
|
2989
|
+
function formatIncident(incident, matches) {
|
|
2990
|
+
const lines = [];
|
|
2991
|
+
const statusColor = getStatusColor(incident.status);
|
|
2992
|
+
lines.push(
|
|
2993
|
+
chalk4.gray("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")
|
|
2994
|
+
);
|
|
2995
|
+
lines.push(
|
|
2996
|
+
chalk4.gray("\u2502 ") + chalk4.bold(`[${incident.id}] `) + statusColor(incident.status.toUpperCase().padEnd(12)) + chalk4.gray(incident.timestamp.substring(0, 19).replace("T", " ").padStart(19)) + chalk4.gray(" \u2502")
|
|
2997
|
+
);
|
|
2998
|
+
lines.push(
|
|
2999
|
+
chalk4.gray("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")
|
|
3000
|
+
);
|
|
3001
|
+
lines.push(chalk4.gray("\u2502 ") + chalk4.red("Error: ") + truncate(incident.error.message, 55) + chalk4.gray(" \u2502"));
|
|
3002
|
+
lines.push(chalk4.gray("\u2502") + " ".repeat(65) + chalk4.gray("\u2502"));
|
|
3003
|
+
lines.push(chalk4.gray("\u2502 ") + chalk4.cyan("Symbolic Context:") + " ".repeat(47) + chalk4.gray("\u2502"));
|
|
3004
|
+
const symbols = formatSymbols(incident.symbols);
|
|
3005
|
+
for (const sym of symbols) {
|
|
3006
|
+
lines.push(chalk4.gray("\u2502 ") + sym.padEnd(61) + chalk4.gray(" \u2502"));
|
|
3007
|
+
}
|
|
3008
|
+
lines.push(chalk4.gray("\u2502") + " ".repeat(65) + chalk4.gray("\u2502"));
|
|
3009
|
+
const envLine = `Environment: ${chalk4.yellow(incident.environment)} \u2502 Service: ${chalk4.yellow(incident.service || "N/A")} \u2502 v${incident.version || "N/A"}`;
|
|
3010
|
+
lines.push(chalk4.gray("\u2502 ") + envLine.substring(0, 63).padEnd(63) + chalk4.gray(" \u2502"));
|
|
3011
|
+
if (matches && matches.length > 0) {
|
|
3012
|
+
lines.push(chalk4.gray("\u2502") + " ".repeat(65) + chalk4.gray("\u2502"));
|
|
3013
|
+
lines.push(
|
|
3014
|
+
chalk4.gray("\u2502 \u250C\u2500 ") + chalk4.cyan("Matched Patterns") + chalk4.gray(" \u2500".repeat(22) + "\u2510 \u2502")
|
|
3015
|
+
);
|
|
3016
|
+
for (let i = 0; i < Math.min(matches.length, 3); i++) {
|
|
3017
|
+
const match = matches[i];
|
|
3018
|
+
const icon = i === 0 ? chalk4.yellow("\u2605") : chalk4.gray("\u25CB");
|
|
3019
|
+
const conf = `${match.confidence}% confidence`;
|
|
3020
|
+
lines.push(
|
|
3021
|
+
chalk4.gray("\u2502 \u2502 ") + icon + " " + chalk4.bold(truncate(match.pattern.id, 30).padEnd(30)) + chalk4.gray(conf.padStart(15)) + chalk4.gray(" \u2502 \u2502")
|
|
3022
|
+
);
|
|
3023
|
+
lines.push(
|
|
3024
|
+
chalk4.gray("\u2502 \u2502 ") + chalk4.italic(truncate(match.pattern.description, 45).padEnd(45)) + chalk4.gray(" \u2502 \u2502")
|
|
3025
|
+
);
|
|
3026
|
+
lines.push(
|
|
3027
|
+
chalk4.gray("\u2502 \u2502 Strategy: ") + chalk4.cyan(match.pattern.resolution.strategy.padEnd(40)) + chalk4.gray(" \u2502 \u2502")
|
|
3028
|
+
);
|
|
3029
|
+
if (i < Math.min(matches.length, 3) - 1) {
|
|
3030
|
+
lines.push(chalk4.gray("\u2502 \u2502") + " ".repeat(59) + chalk4.gray("\u2502 \u2502"));
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
lines.push(
|
|
3034
|
+
chalk4.gray("\u2502 \u2514") + "\u2500".repeat(59) + chalk4.gray("\u2518 \u2502")
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
lines.push(chalk4.gray("\u2502") + " ".repeat(65) + chalk4.gray("\u2502"));
|
|
3038
|
+
lines.push(chalk4.gray("\u2502 ") + chalk4.dim("Actions:") + " ".repeat(56) + chalk4.gray("\u2502"));
|
|
3039
|
+
lines.push(
|
|
3040
|
+
chalk4.gray("\u2502 ") + chalk4.dim(`sentinel triage resolve ${incident.id}`) + " ".repeat(36 - incident.id.length) + chalk4.gray("\u2502")
|
|
3041
|
+
);
|
|
3042
|
+
lines.push(
|
|
3043
|
+
chalk4.gray("\u2502 ") + chalk4.dim(`sentinel triage show ${incident.id} --timeline`) + " ".repeat(29 - incident.id.length) + chalk4.gray("\u2502")
|
|
3044
|
+
);
|
|
3045
|
+
lines.push(
|
|
3046
|
+
chalk4.gray("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")
|
|
3047
|
+
);
|
|
3048
|
+
return lines.join("\n");
|
|
3049
|
+
}
|
|
3050
|
+
function formatIncidentCompact(incident) {
|
|
3051
|
+
const statusColor = getStatusColor(incident.status);
|
|
3052
|
+
const status = statusColor(incident.status.substring(0, 4).toUpperCase().padEnd(4));
|
|
3053
|
+
const timestamp = incident.timestamp.substring(5, 16).replace("T", " ");
|
|
3054
|
+
const error = truncate(incident.error.message, 40);
|
|
3055
|
+
const symbols = Object.values(incident.symbols).filter(Boolean).join(" ");
|
|
3056
|
+
return `${chalk4.bold(incident.id)} ${status} ${chalk4.gray(timestamp)} ${error}
|
|
3057
|
+
${chalk4.cyan(truncate(symbols, 60))}`;
|
|
3058
|
+
}
|
|
3059
|
+
function formatStats(stats) {
|
|
3060
|
+
const lines = [];
|
|
3061
|
+
lines.push(chalk4.bold.cyan("\nIncident Statistics"));
|
|
3062
|
+
lines.push(chalk4.gray("\u2500".repeat(50)));
|
|
3063
|
+
lines.push(` Total: ${chalk4.white(stats.incidents.total)}`);
|
|
3064
|
+
lines.push(` Open: ${chalk4.yellow(stats.incidents.open)}`);
|
|
3065
|
+
lines.push(` Resolved: ${chalk4.green(stats.incidents.resolved)}`);
|
|
3066
|
+
lines.push("");
|
|
3067
|
+
lines.push(chalk4.bold.cyan("By Environment"));
|
|
3068
|
+
for (const [env, count] of Object.entries(stats.incidents.byEnvironment)) {
|
|
3069
|
+
lines.push(` ${env}: ${count}`);
|
|
3070
|
+
}
|
|
3071
|
+
lines.push("");
|
|
3072
|
+
lines.push(chalk4.bold.cyan("Resolution Stats"));
|
|
3073
|
+
lines.push(chalk4.gray("\u2500".repeat(50)));
|
|
3074
|
+
lines.push(` Resolution Rate: ${chalk4.green(Math.round(stats.resolution.resolutionRate) + "%")}`);
|
|
3075
|
+
lines.push(` Resolved with Pattern: ${stats.resolution.resolvedWithPattern}`);
|
|
3076
|
+
lines.push(` Resolved Manually: ${stats.resolution.resolvedManually}`);
|
|
3077
|
+
lines.push("");
|
|
3078
|
+
lines.push(chalk4.bold.cyan("Top Affected Symbols"));
|
|
3079
|
+
lines.push(chalk4.gray("\u2500".repeat(50)));
|
|
3080
|
+
for (const { symbol, count } of stats.symbols.mostIncidents.slice(0, 5)) {
|
|
3081
|
+
lines.push(` ${chalk4.cyan(symbol.padEnd(30))} ${count} incidents`);
|
|
3082
|
+
}
|
|
3083
|
+
lines.push("");
|
|
3084
|
+
lines.push(chalk4.bold.cyan("Most Effective Patterns"));
|
|
3085
|
+
lines.push(chalk4.gray("\u2500".repeat(50)));
|
|
3086
|
+
for (const { patternId, resolvedCount } of stats.patterns.mostEffective.slice(0, 5)) {
|
|
3087
|
+
lines.push(` ${patternId.padEnd(30)} ${resolvedCount} resolved`);
|
|
3088
|
+
}
|
|
3089
|
+
return lines.join("\n");
|
|
3090
|
+
}
|
|
3091
|
+
function formatSymbols(symbols) {
|
|
3092
|
+
const result = [];
|
|
3093
|
+
for (const [key, value] of Object.entries(symbols)) {
|
|
3094
|
+
if (value) {
|
|
3095
|
+
const color = getSymbolColor(key);
|
|
3096
|
+
result.push(`${color(value.padEnd(20))} ${chalk4.dim(key)}`);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
return result;
|
|
3100
|
+
}
|
|
3101
|
+
function getStatusColor(status) {
|
|
3102
|
+
switch (status) {
|
|
3103
|
+
case "open":
|
|
3104
|
+
return chalk4.red;
|
|
3105
|
+
case "investigating":
|
|
3106
|
+
return chalk4.yellow;
|
|
3107
|
+
case "resolved":
|
|
3108
|
+
return chalk4.green;
|
|
3109
|
+
case "wont-fix":
|
|
3110
|
+
return chalk4.gray;
|
|
3111
|
+
default:
|
|
3112
|
+
return chalk4.white;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
function getSymbolColor(type) {
|
|
3116
|
+
switch (type) {
|
|
3117
|
+
case "feature":
|
|
3118
|
+
return chalk4.magenta;
|
|
3119
|
+
case "component":
|
|
3120
|
+
return chalk4.blue;
|
|
3121
|
+
case "flow":
|
|
3122
|
+
return chalk4.cyan;
|
|
3123
|
+
case "gate":
|
|
3124
|
+
return chalk4.yellow;
|
|
3125
|
+
case "signal":
|
|
3126
|
+
return chalk4.green;
|
|
3127
|
+
case "state":
|
|
3128
|
+
return chalk4.red;
|
|
3129
|
+
case "integration":
|
|
3130
|
+
return chalk4.white;
|
|
3131
|
+
default:
|
|
3132
|
+
return chalk4.gray;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
function truncate(str, maxLen) {
|
|
3136
|
+
if (str.length <= maxLen) return str;
|
|
3137
|
+
return str.substring(0, maxLen - 3) + "...";
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
// src/cli/commands.ts
|
|
3141
|
+
async function getStorage() {
|
|
3142
|
+
const storage = new SentinelStorage();
|
|
3143
|
+
await storage.ensureReady();
|
|
3144
|
+
try {
|
|
3145
|
+
const { patterns } = loadAllSeedPatterns();
|
|
3146
|
+
for (const pattern of patterns) {
|
|
3147
|
+
try {
|
|
3148
|
+
storage.addPattern(pattern);
|
|
3149
|
+
} catch {
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
} catch {
|
|
3153
|
+
}
|
|
3154
|
+
return storage;
|
|
3155
|
+
}
|
|
3156
|
+
async function launchDashboard(opts) {
|
|
3157
|
+
console.log(chalk5.cyan("\n Sentinel Dashboard\n"));
|
|
3158
|
+
await startServer({
|
|
3159
|
+
port: parseInt(opts.port, 10),
|
|
3160
|
+
projectDir: process.cwd(),
|
|
3161
|
+
open: opts.open
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
async function initProject(opts) {
|
|
3165
|
+
console.log(chalk5.cyan("\n Initializing Sentinel...\n"));
|
|
3166
|
+
const projectDir = process.cwd();
|
|
3167
|
+
const config = generateConfig(projectDir);
|
|
3168
|
+
writeConfig(projectDir, config);
|
|
3169
|
+
console.log(chalk5.green(" Created .sentinel.yaml"));
|
|
3170
|
+
if (config.symbols) {
|
|
3171
|
+
const total = (config.symbols.components?.length || 0) + (config.symbols.gates?.length || 0) + (config.symbols.flows?.length || 0) + (config.symbols.signals?.length || 0);
|
|
3172
|
+
if (total > 0) {
|
|
3173
|
+
console.log(chalk5.gray(` Detected ${total} symbols`));
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
const storage = new SentinelStorage();
|
|
3177
|
+
await storage.ensureReady();
|
|
3178
|
+
storage.close();
|
|
3179
|
+
console.log(chalk5.green(" Initialized database"));
|
|
3180
|
+
console.log(chalk5.gray("\n Run `sentinel` to launch the dashboard\n"));
|
|
3181
|
+
}
|
|
3182
|
+
async function triageList(opts) {
|
|
3183
|
+
const storage = await getStorage();
|
|
3184
|
+
const matcher = new PatternMatcher(storage);
|
|
3185
|
+
const incidents = storage.getRecentIncidents({
|
|
3186
|
+
limit: parseInt(opts.limit, 10),
|
|
3187
|
+
status: opts.status || "all",
|
|
3188
|
+
environment: opts.env,
|
|
3189
|
+
symbol: opts.symbol
|
|
3190
|
+
});
|
|
3191
|
+
console.log(formatHeader());
|
|
3192
|
+
if (incidents.length === 0) {
|
|
3193
|
+
console.log(chalk5.gray(" No incidents found.\n"));
|
|
3194
|
+
console.log(chalk5.dim(" Record incidents via the SDK:"));
|
|
3195
|
+
console.log(chalk5.dim(' sentinel.capture(error, { component: "#checkout" })'));
|
|
3196
|
+
console.log(chalk5.dim(" Or via MCP:"));
|
|
3197
|
+
console.log(chalk5.dim(' sentinel_record({ error: { message: "..." }, symbols: { component: "#checkout" }, environment: "production" })\n'));
|
|
3198
|
+
storage.close();
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
const all = storage.getRecentIncidents({ limit: 1e4 });
|
|
3202
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().substring(0, 10);
|
|
3203
|
+
console.log(formatSummaryBar({
|
|
3204
|
+
open: all.filter((i) => i.status === "open").length,
|
|
3205
|
+
investigating: all.filter((i) => i.status === "investigating").length,
|
|
3206
|
+
resolved: all.filter((i) => i.status === "resolved").length,
|
|
3207
|
+
today: all.filter((i) => i.timestamp.startsWith(today)).length
|
|
3208
|
+
}));
|
|
3209
|
+
for (const incident of incidents) {
|
|
3210
|
+
const matches = matcher.match(incident, { maxResults: 3 });
|
|
3211
|
+
console.log(formatIncidentCompact(incident));
|
|
3212
|
+
}
|
|
3213
|
+
console.log("");
|
|
3214
|
+
storage.close();
|
|
3215
|
+
}
|
|
3216
|
+
async function triageShow(id, opts) {
|
|
3217
|
+
const storage = await getStorage();
|
|
3218
|
+
const matcher = new PatternMatcher(storage);
|
|
3219
|
+
const incident = storage.getIncident(id);
|
|
3220
|
+
if (!incident) {
|
|
3221
|
+
console.log(chalk5.red(`
|
|
3222
|
+
Incident ${id} not found
|
|
3223
|
+
`));
|
|
3224
|
+
storage.close();
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
const matches = matcher.match(incident, { maxResults: 5 });
|
|
3228
|
+
console.log("");
|
|
3229
|
+
console.log(formatIncident(incident, matches));
|
|
3230
|
+
if (opts.timeline && incident.flowPosition) {
|
|
3231
|
+
const builder = new TimelineBuilder();
|
|
3232
|
+
const timeline = builder.build(incident);
|
|
3233
|
+
if (timeline) {
|
|
3234
|
+
console.log("\n" + builder.renderAscii(timeline));
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
console.log("");
|
|
3238
|
+
storage.close();
|
|
3239
|
+
}
|
|
3240
|
+
async function triageResolve(id, opts) {
|
|
3241
|
+
const storage = await getStorage();
|
|
3242
|
+
const incident = storage.getIncident(id);
|
|
3243
|
+
if (!incident) {
|
|
3244
|
+
console.log(chalk5.red(`
|
|
3245
|
+
Incident ${id} not found
|
|
3246
|
+
`));
|
|
3247
|
+
storage.close();
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
storage.recordResolution({
|
|
3251
|
+
incidentId: id,
|
|
3252
|
+
patternId: opts.pattern,
|
|
3253
|
+
commitHash: opts.commit,
|
|
3254
|
+
notes: opts.notes
|
|
3255
|
+
});
|
|
3256
|
+
console.log(chalk5.green(`
|
|
3257
|
+
Incident ${id} resolved
|
|
3258
|
+
`));
|
|
3259
|
+
storage.close();
|
|
3260
|
+
}
|
|
3261
|
+
async function triageStats(opts) {
|
|
3262
|
+
const storage = await getStorage();
|
|
3263
|
+
const calculator = new StatsCalculator(storage);
|
|
3264
|
+
const match = opts.period.match(/^(\d+)d$/);
|
|
3265
|
+
const days = match ? parseInt(match[1], 10) : 7;
|
|
3266
|
+
const stats = calculator.getStats(days);
|
|
3267
|
+
console.log(formatStats(stats));
|
|
3268
|
+
console.log("");
|
|
3269
|
+
storage.close();
|
|
3270
|
+
}
|
|
3271
|
+
export {
|
|
3272
|
+
initProject,
|
|
3273
|
+
launchDashboard,
|
|
3274
|
+
triageList,
|
|
3275
|
+
triageResolve,
|
|
3276
|
+
triageShow,
|
|
3277
|
+
triageStats
|
|
3278
|
+
};
|