@a-company/paradigm 3.12.0 → 3.13.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/{accept-orchestration-YKMKMWGA.js → accept-orchestration-ORQRKKGR.js} +4 -4
- package/dist/{agents-suggest-35LIQKDH.js → agents-suggest-65SER5IS.js} +1 -1
- package/dist/{aggregate-V4KPR3RW.js → aggregate-M5WMUI6B.js} +1 -1
- package/dist/{auto-6MOGYQ4G.js → auto-B22FVSQI.js} +1 -1
- package/dist/{beacon-XRXL5KZB.js → beacon-XL2ALH5O.js} +1 -1
- package/dist/{check-UZY647TB.js → check-5RKOAN7S.js} +1 -1
- package/dist/{chunk-RDPXBMHK.js → chunk-3BAMPB6I.js} +6 -6
- package/dist/{chunk-5TUAVVIG.js → chunk-6GWRQWQB.js} +1 -1
- package/dist/{chunk-F6EJKLF4.js → chunk-O5ZO5LSW.js} +1 -1
- package/dist/{chunk-UVI3OH3G.js → chunk-R2SGQ22F.js} +11 -58
- package/dist/{chunk-4PEQHWD7.js → chunk-XKAFTZOZ.js} +1 -1
- package/dist/{chunk-5N5LR2KS.js → chunk-XNUWLW73.js} +6 -6
- package/dist/{chunk-JQYGPVLQ.js → chunk-YOFP72IB.js} +1 -1
- package/dist/{chunk-CQFNTBFJ.js → chunk-Z42FOOVT.js} +3 -3
- package/dist/{chunk-CYGHL7PQ.js → chunk-ZMN3RAIT.js} +5 -5
- package/dist/{chunk-4ZO3ZOPM.js → chunk-ZRPEI35Q.js} +14 -59
- package/dist/chunk-ZXMDA7VB.js +16 -0
- package/dist/{claude-SUYNN72C.js → claude-63ISJAZK.js} +1 -1
- package/dist/{claude-cli-OF43XAO3.js → claude-cli-ABML5RHX.js} +1 -1
- package/dist/{claude-code-PW6SKD2M.js → claude-code-JRLMRPTO.js} +1 -1
- package/dist/{claude-code-teams-JLZ5IXB6.js → claude-code-teams-CAJBEFIZ.js} +1 -1
- package/dist/commands-VTFOZPUA.js +5387 -0
- package/dist/{constellation-GNK5DIMH.js → constellation-NXU6Q2HM.js} +1 -1
- package/dist/{cost-AGO5N7DD.js → cost-CTGSLSOC.js} +1 -1
- package/dist/{cost-KYXIQ62X.js → cost-XEBADYFT.js} +1 -1
- package/dist/{cursor-cli-IHJMPRCW.js → cursor-cli-QUOOF2N4.js} +1 -1
- package/dist/{cursorrules-LQFA7M62.js → cursorrules-XBWFX66V.js} +1 -1
- package/dist/{delete-3YXAJ5AA.js → delete-OINCSDQH.js} +2 -2
- package/dist/{diff-4FV7T35U.js → diff-4XJZN4OB.js} +4 -4
- package/dist/{dist-ZEMSQV74.js → dist-3RVKEJRT.js} +1 -1
- package/dist/{dist-Q6SAZI7X.js → dist-7U64HDSC.js} +1 -1
- package/dist/{dist-AG5JNIZU-XSEZ2LLK.js → dist-AG5JNIZU-HW2FWNTZ.js} +1 -1
- package/dist/dist-KY5HGDDL.js +1304 -0
- package/dist/{dist-JOHRYQUA.js → dist-PSF5CP4I.js} +1 -1
- package/dist/{dist-6SX5ZKKF.js → dist-RMAIFRTW.js} +3 -3
- package/dist/{dist-YB7T54QE.js → dist-YHDSIZQD.js} +1 -1
- package/dist/{doctor-2KM5HOK6.js → doctor-FINKMI66.js} +2 -2
- package/dist/{drift-FH2UY64B.js → drift-YGT4LJ7Q.js} +1 -1
- package/dist/{echo-VYZW3OTT.js → echo-A6HD5UP7.js} +1 -1
- package/dist/{edit-EOMPXOG5.js → edit-7FSQNAPE.js} +2 -2
- package/dist/{export-R4FJ5NOH.js → export-T7CMMJIB.js} +1 -1
- package/dist/{flow-MCKPJGRJ.js → flow-UFMPVOEM.js} +1 -1
- package/dist/{global-AXILUM5X.js → global-HHUJSBG5.js} +1 -1
- package/dist/{habits-NC2TRMRV.js → habits-KD4RLIN2.js} +3 -3
- package/dist/{history-EVO3L6SC.js → history-CETCSUCP.js} +1 -1
- package/dist/{hooks-JXYHVGIN.js → hooks-TCUHQMPF.js} +1 -1
- package/dist/index.js +138 -137
- package/dist/{lint-N4LMMEXH.js → lint-53GPXKKI.js} +1 -1
- package/dist/{list-JKBJ7ESH.js → list-Q4R7L7WJ.js} +2 -2
- package/dist/{lore-server-RQH5REZV.js → lore-server-GKZ6ESNJ.js} +1 -1
- package/dist/{manual-Y3QOXWYA.js → manual-AFJ2J2V3.js} +1 -1
- package/dist/mcp.js +4 -4
- package/dist/{orchestrate-IV54FMHD.js → orchestrate-6XGEA655.js} +4 -4
- package/dist/{portal-check-2HI4FFD6.js → portal-check-FF5EKZE5.js} +1 -1
- package/dist/{portal-compliance-KQCTAQTJ.js → portal-compliance-VU4NIFEN.js} +1 -1
- package/dist/{probe-X3J2JX62.js → probe-T77FFIAG.js} +1 -1
- package/dist/{promote-HZH5E5CO.js → promote-XO63XMAN.js} +2 -2
- package/dist/{providers-IONB4YRJ.js → providers-VIBWDN5D.js} +2 -2
- package/dist/{record-EECZ3E4I.js → record-YJ3D3462.js} +2 -2
- package/dist/{reindex-ZM6J53UP.js → reindex-4OOME3TT.js} +1 -1
- package/dist/{remember-3KJZGDUG.js → remember-IEBQHXHZ.js} +1 -1
- package/dist/{review-BF26ILZB.js → review-3OW3KVW7.js} +2 -2
- package/dist/{ripple-JIUAMBLA.js → ripple-DFMXLFWI.js} +1 -1
- package/dist/{sentinel-BGCISNIK.js → sentinel-RERNMWSE.js} +2 -2
- package/dist/sentinel-mcp.js +4181 -0
- package/dist/sentinel.js +35 -0
- package/dist/{serve-H7ZBMODT.js → serve-XLKEMQEH.js} +2 -2
- package/dist/server-CAXNYVV7.js +1616 -0
- package/dist/{server-E2CNZC4K.js → server-V3ANAXDP.js} +1 -1
- package/dist/{setup-UKJ3VGHI.js → setup-HOI52TN3.js} +2 -2
- package/dist/{setup-363IB6MO.js → setup-YNZJQLW7.js} +1 -1
- package/dist/{shift-G2ZCIR5Q.js → shift-SW3GSODO.js} +7 -7
- package/dist/{show-SAMTXEHG.js → show-CJGHREFS.js} +2 -2
- package/dist/{snapshot-KCMONZAO.js → snapshot-XHINQBZS.js} +1 -1
- package/dist/{spawn-7SDONTJN.js → spawn-JSV2HST3.js} +3 -3
- package/dist/{summary-F46FRO3Y.js → summary-NV7SBV5O.js} +1 -1
- package/dist/{switch-CC2KACXO.js → switch-WYUMVNA5.js} +1 -1
- package/dist/{sync-4CNRHUWX.js → sync-ZM4Q3R4U.js} +1 -1
- package/dist/{sync-llms-MCWB37HN.js → sync-llms-JIPP3XX4.js} +1 -1
- package/dist/{team-XUZBPIFZ.js → team-YIYA4ZLX.js} +5 -5
- package/dist/{test-DK2RWLTK.js → test-WTR5Q33E.js} +1 -1
- package/dist/{thread-RNSLADXN.js → thread-3WM7KKID.js} +1 -1
- package/dist/{timeline-TJDVVVA3.js → timeline-ELO5JTQO.js} +2 -2
- package/dist/{triage-MKKIWBSW.js → triage-GJ6GK647.js} +3 -3
- package/dist/{tutorial-L5Q3ZDHK.js → tutorial-GC6QL4US.js} +1 -1
- package/dist/{university-65YJZ2LW.js → university-KVYNACJZ.js} +2 -2
- package/dist/{upgrade-HGF4MBGV.js → upgrade-65QOQXRC.js} +1 -1
- package/dist/{validate-F3YHBCRZ.js → validate-ITC5D6QG.js} +1 -1
- package/dist/{validate-2LTHHORX.js → validate-TKKRGJKC.js} +1 -1
- package/dist/{watch-CL2PPS2K.js → watch-ERBEJUJW.js} +1 -1
- package/dist/{watch-NBPOMOEX.js → watch-X64UK7K4.js} +2 -2
- package/dist/{wisdom-LRM4FFCH.js → wisdom-L2WC7J62.js} +1 -1
- package/dist/{workspace-7CWY4IWV.js → workspace-S5Q5LVA6.js} +1 -1
- package/package.json +7 -2
- package/dist/chunk-MO4EEYFW.js +0 -38
- package/dist/server-3K3TTJH3.js +0 -10539
|
@@ -0,0 +1,4181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../sentinel/src/mcp.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
|
|
11
|
+
// ../sentinel/src/storage.ts
|
|
12
|
+
import initSqlJs from "sql.js";
|
|
13
|
+
import { v4 as uuidv4 } from "uuid";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
var SCHEMA_VERSION = 5;
|
|
17
|
+
var DEFAULT_CONFIDENCE = {
|
|
18
|
+
score: 50,
|
|
19
|
+
timesMatched: 0,
|
|
20
|
+
timesResolved: 0,
|
|
21
|
+
timesRecurred: 0
|
|
22
|
+
};
|
|
23
|
+
var SQL = null;
|
|
24
|
+
var SentinelStorage = class {
|
|
25
|
+
db = null;
|
|
26
|
+
dbPath;
|
|
27
|
+
incidentCounter = 0;
|
|
28
|
+
initialized = false;
|
|
29
|
+
constructor(dbPath) {
|
|
30
|
+
this.dbPath = dbPath || this.getDefaultDbPath();
|
|
31
|
+
}
|
|
32
|
+
getDefaultDbPath() {
|
|
33
|
+
const dataDir = process.env.SENTINEL_DATA_DIR || process.env.PARADIGM_DATA_DIR || path.join(process.cwd(), ".paradigm", "sentinel");
|
|
34
|
+
return path.join(dataDir, "sentinel.db");
|
|
35
|
+
}
|
|
36
|
+
createSchema() {
|
|
37
|
+
if (!this.db) return;
|
|
38
|
+
this.db.run(`
|
|
39
|
+
-- Metadata table for schema versioning
|
|
40
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
41
|
+
key TEXT PRIMARY KEY,
|
|
42
|
+
value TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
-- Incidents table
|
|
46
|
+
CREATE TABLE IF NOT EXISTS incidents (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
timestamp TEXT NOT NULL,
|
|
49
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
50
|
+
error_message TEXT NOT NULL,
|
|
51
|
+
error_stack TEXT,
|
|
52
|
+
error_code TEXT,
|
|
53
|
+
error_type TEXT,
|
|
54
|
+
symbols TEXT NOT NULL,
|
|
55
|
+
flow_position TEXT,
|
|
56
|
+
environment TEXT NOT NULL,
|
|
57
|
+
service TEXT,
|
|
58
|
+
version TEXT,
|
|
59
|
+
user_id TEXT,
|
|
60
|
+
request_id TEXT,
|
|
61
|
+
group_id TEXT,
|
|
62
|
+
notes TEXT DEFAULT '[]',
|
|
63
|
+
related_incidents TEXT DEFAULT '[]',
|
|
64
|
+
resolved_at TEXT,
|
|
65
|
+
resolved_by TEXT,
|
|
66
|
+
resolution TEXT,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
updated_at TEXT NOT NULL
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- Patterns table
|
|
72
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
name TEXT NOT NULL,
|
|
75
|
+
description TEXT,
|
|
76
|
+
pattern TEXT NOT NULL,
|
|
77
|
+
resolution TEXT NOT NULL,
|
|
78
|
+
confidence TEXT NOT NULL,
|
|
79
|
+
source TEXT NOT NULL DEFAULT 'manual',
|
|
80
|
+
private INTEGER NOT NULL DEFAULT 0,
|
|
81
|
+
tags TEXT DEFAULT '[]',
|
|
82
|
+
created_at TEXT NOT NULL,
|
|
83
|
+
updated_at TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
-- Incident groups
|
|
87
|
+
CREATE TABLE IF NOT EXISTS groups (
|
|
88
|
+
id TEXT PRIMARY KEY,
|
|
89
|
+
name TEXT,
|
|
90
|
+
common_symbols TEXT,
|
|
91
|
+
common_error_patterns TEXT,
|
|
92
|
+
suggested_pattern_id TEXT,
|
|
93
|
+
first_seen TEXT NOT NULL,
|
|
94
|
+
last_seen TEXT NOT NULL,
|
|
95
|
+
created_at TEXT NOT NULL,
|
|
96
|
+
updated_at TEXT NOT NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
-- Group members
|
|
100
|
+
CREATE TABLE IF NOT EXISTS group_members (
|
|
101
|
+
group_id TEXT NOT NULL,
|
|
102
|
+
incident_id TEXT NOT NULL,
|
|
103
|
+
added_at TEXT NOT NULL,
|
|
104
|
+
PRIMARY KEY (group_id, incident_id)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
-- Resolutions history
|
|
108
|
+
CREATE TABLE IF NOT EXISTS resolutions (
|
|
109
|
+
id TEXT PRIMARY KEY,
|
|
110
|
+
incident_id TEXT NOT NULL,
|
|
111
|
+
pattern_id TEXT,
|
|
112
|
+
commit_hash TEXT,
|
|
113
|
+
pr_url TEXT,
|
|
114
|
+
notes TEXT,
|
|
115
|
+
resolved_at TEXT NOT NULL,
|
|
116
|
+
recurred INTEGER DEFAULT 0
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
-- Practice events (habits system)
|
|
120
|
+
CREATE TABLE IF NOT EXISTS practice_events (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
timestamp TEXT NOT NULL,
|
|
123
|
+
habit_id TEXT NOT NULL,
|
|
124
|
+
habit_category TEXT NOT NULL,
|
|
125
|
+
result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
|
|
126
|
+
engineer TEXT NOT NULL,
|
|
127
|
+
session_id TEXT NOT NULL,
|
|
128
|
+
lore_entry_id TEXT,
|
|
129
|
+
task_description TEXT,
|
|
130
|
+
symbols_touched TEXT DEFAULT '[]',
|
|
131
|
+
files_modified TEXT DEFAULT '[]',
|
|
132
|
+
related_incident_id TEXT,
|
|
133
|
+
notes TEXT
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
-- Structured logs table
|
|
137
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
138
|
+
id TEXT PRIMARY KEY,
|
|
139
|
+
timestamp TEXT NOT NULL,
|
|
140
|
+
level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
|
|
141
|
+
symbol TEXT NOT NULL,
|
|
142
|
+
symbol_type TEXT NOT NULL DEFAULT 'raw',
|
|
143
|
+
message TEXT NOT NULL,
|
|
144
|
+
data_json TEXT,
|
|
145
|
+
service TEXT NOT NULL,
|
|
146
|
+
session_id TEXT,
|
|
147
|
+
correlation_id TEXT,
|
|
148
|
+
duration_ms REAL,
|
|
149
|
+
environment TEXT
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
-- Service registry
|
|
153
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
154
|
+
name TEXT PRIMARY KEY,
|
|
155
|
+
version TEXT,
|
|
156
|
+
pid INTEGER,
|
|
157
|
+
started_at TEXT NOT NULL,
|
|
158
|
+
last_seen_at TEXT NOT NULL,
|
|
159
|
+
environment TEXT,
|
|
160
|
+
metadata_json TEXT
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
-- Live app state snapshots (latest-wins per service+session)
|
|
164
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
165
|
+
service TEXT NOT NULL,
|
|
166
|
+
session_id TEXT NOT NULL,
|
|
167
|
+
timestamp TEXT NOT NULL,
|
|
168
|
+
state_json TEXT NOT NULL,
|
|
169
|
+
active_flows_json TEXT,
|
|
170
|
+
active_gates_json TEXT,
|
|
171
|
+
PRIMARY KEY (service, session_id)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
-- Metrics table
|
|
175
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
176
|
+
id TEXT PRIMARY KEY,
|
|
177
|
+
timestamp TEXT NOT NULL,
|
|
178
|
+
name TEXT NOT NULL,
|
|
179
|
+
type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
|
|
180
|
+
value REAL NOT NULL,
|
|
181
|
+
tags_json TEXT DEFAULT '{}',
|
|
182
|
+
service TEXT NOT NULL,
|
|
183
|
+
environment TEXT
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
-- Traces table
|
|
187
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
188
|
+
trace_id TEXT NOT NULL,
|
|
189
|
+
span_id TEXT PRIMARY KEY,
|
|
190
|
+
parent_span_id TEXT,
|
|
191
|
+
service TEXT NOT NULL,
|
|
192
|
+
symbol TEXT NOT NULL,
|
|
193
|
+
operation TEXT NOT NULL,
|
|
194
|
+
start_time TEXT NOT NULL,
|
|
195
|
+
end_time TEXT,
|
|
196
|
+
duration_ms REAL,
|
|
197
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
198
|
+
tags_json TEXT DEFAULT '{}',
|
|
199
|
+
log_ids_json TEXT DEFAULT '[]'
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
-- Indexes
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_environment ON incidents(environment);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_source ON patterns(source);
|
|
207
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
|
|
212
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
213
|
+
CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
|
|
215
|
+
CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
|
|
216
|
+
CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
|
|
223
|
+
`);
|
|
224
|
+
this.db.run(
|
|
225
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
|
|
226
|
+
[String(SCHEMA_VERSION)]
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
save() {
|
|
230
|
+
if (!this.db) return;
|
|
231
|
+
const data = this.db.export();
|
|
232
|
+
const buffer = Buffer.from(data);
|
|
233
|
+
fs.writeFileSync(this.dbPath, buffer);
|
|
234
|
+
}
|
|
235
|
+
// ─── Incidents ───────────────────────────────────────────────────
|
|
236
|
+
recordIncident(input) {
|
|
237
|
+
const db = this.db;
|
|
238
|
+
if (!db) {
|
|
239
|
+
this.initializeSync();
|
|
240
|
+
}
|
|
241
|
+
this.incidentCounter++;
|
|
242
|
+
const id = `INC-${String(this.incidentCounter).padStart(3, "0")}`;
|
|
243
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
244
|
+
this.db.run(
|
|
245
|
+
`INSERT INTO incidents (
|
|
246
|
+
id, timestamp, status, error_message, error_stack, error_code, error_type,
|
|
247
|
+
symbols, flow_position, environment, service, version, user_id, request_id,
|
|
248
|
+
group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
|
|
249
|
+
created_at, updated_at
|
|
250
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
251
|
+
[
|
|
252
|
+
id,
|
|
253
|
+
input.timestamp || now,
|
|
254
|
+
input.status || "open",
|
|
255
|
+
input.error.message,
|
|
256
|
+
input.error.stack || null,
|
|
257
|
+
input.error.code || null,
|
|
258
|
+
input.error.type || null,
|
|
259
|
+
JSON.stringify(input.symbols),
|
|
260
|
+
input.flowPosition ? JSON.stringify(input.flowPosition) : null,
|
|
261
|
+
input.environment,
|
|
262
|
+
input.service || null,
|
|
263
|
+
input.version || null,
|
|
264
|
+
input.userId || null,
|
|
265
|
+
input.requestId || null,
|
|
266
|
+
input.groupId || null,
|
|
267
|
+
"[]",
|
|
268
|
+
"[]",
|
|
269
|
+
input.resolvedAt || null,
|
|
270
|
+
input.resolvedBy || null,
|
|
271
|
+
input.resolution ? JSON.stringify(input.resolution) : null,
|
|
272
|
+
now,
|
|
273
|
+
now
|
|
274
|
+
]
|
|
275
|
+
);
|
|
276
|
+
this.save();
|
|
277
|
+
return id;
|
|
278
|
+
}
|
|
279
|
+
initializeSync() {
|
|
280
|
+
if (this.initialized && this.db) return;
|
|
281
|
+
if (!this.db && SQL) {
|
|
282
|
+
const dir = path.dirname(this.dbPath);
|
|
283
|
+
if (!fs.existsSync(dir)) {
|
|
284
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
if (fs.existsSync(this.dbPath)) {
|
|
287
|
+
const fileData = fs.readFileSync(this.dbPath);
|
|
288
|
+
this.db = new SQL.Database(fileData);
|
|
289
|
+
} else {
|
|
290
|
+
this.db = new SQL.Database();
|
|
291
|
+
this.createSchema();
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const result = this.db.exec(
|
|
295
|
+
"SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
|
|
296
|
+
);
|
|
297
|
+
if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
|
|
298
|
+
this.incidentCounter = result[0].values[0][0];
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
this.incidentCounter = 0;
|
|
302
|
+
}
|
|
303
|
+
this.migrateSchema();
|
|
304
|
+
this.initialized = true;
|
|
305
|
+
this.save();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Run schema migrations from older versions
|
|
310
|
+
*/
|
|
311
|
+
migrateSchema() {
|
|
312
|
+
if (!this.db) return;
|
|
313
|
+
let currentVersion = 1;
|
|
314
|
+
try {
|
|
315
|
+
const result = this.db.exec(
|
|
316
|
+
"SELECT value FROM metadata WHERE key = 'schema_version'"
|
|
317
|
+
);
|
|
318
|
+
if (result.length > 0 && result[0].values.length > 0) {
|
|
319
|
+
currentVersion = parseInt(result[0].values[0][0], 10) || 1;
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
if (currentVersion < 2) {
|
|
324
|
+
try {
|
|
325
|
+
this.db.run(`
|
|
326
|
+
CREATE TABLE IF NOT EXISTS practice_events (
|
|
327
|
+
id TEXT PRIMARY KEY,
|
|
328
|
+
timestamp TEXT NOT NULL,
|
|
329
|
+
habit_id TEXT NOT NULL,
|
|
330
|
+
habit_category TEXT NOT NULL,
|
|
331
|
+
result TEXT NOT NULL CHECK (result IN ('followed', 'skipped', 'partial')),
|
|
332
|
+
engineer TEXT NOT NULL,
|
|
333
|
+
session_id TEXT NOT NULL,
|
|
334
|
+
lore_entry_id TEXT,
|
|
335
|
+
task_description TEXT,
|
|
336
|
+
symbols_touched TEXT DEFAULT '[]',
|
|
337
|
+
files_modified TEXT DEFAULT '[]',
|
|
338
|
+
related_incident_id TEXT,
|
|
339
|
+
notes TEXT
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_timestamp ON practice_events(timestamp);
|
|
343
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
|
|
344
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
|
|
345
|
+
CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
|
|
346
|
+
`);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
this.db.run(
|
|
350
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
|
|
351
|
+
);
|
|
352
|
+
currentVersion = 2;
|
|
353
|
+
}
|
|
354
|
+
if (currentVersion < 3) {
|
|
355
|
+
try {
|
|
356
|
+
this.db.run(`
|
|
357
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
358
|
+
id TEXT PRIMARY KEY,
|
|
359
|
+
timestamp TEXT NOT NULL,
|
|
360
|
+
level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
|
|
361
|
+
symbol TEXT NOT NULL,
|
|
362
|
+
symbol_type TEXT NOT NULL DEFAULT 'raw',
|
|
363
|
+
message TEXT NOT NULL,
|
|
364
|
+
data_json TEXT,
|
|
365
|
+
service TEXT NOT NULL,
|
|
366
|
+
session_id TEXT,
|
|
367
|
+
correlation_id TEXT,
|
|
368
|
+
duration_ms REAL,
|
|
369
|
+
environment TEXT
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
373
|
+
name TEXT PRIMARY KEY,
|
|
374
|
+
version TEXT,
|
|
375
|
+
pid INTEGER,
|
|
376
|
+
started_at TEXT NOT NULL,
|
|
377
|
+
last_seen_at TEXT NOT NULL,
|
|
378
|
+
environment TEXT,
|
|
379
|
+
metadata_json TEXT
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
383
|
+
service TEXT NOT NULL,
|
|
384
|
+
session_id TEXT NOT NULL,
|
|
385
|
+
timestamp TEXT NOT NULL,
|
|
386
|
+
state_json TEXT NOT NULL,
|
|
387
|
+
active_flows_json TEXT,
|
|
388
|
+
active_gates_json TEXT,
|
|
389
|
+
PRIMARY KEY (service, session_id)
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
|
|
393
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
|
|
394
|
+
CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
|
|
395
|
+
CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
|
|
396
|
+
CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
|
|
397
|
+
CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
|
|
398
|
+
`);
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
this.db.run(
|
|
402
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '3')"
|
|
403
|
+
);
|
|
404
|
+
currentVersion = 3;
|
|
405
|
+
}
|
|
406
|
+
if (currentVersion < 4) {
|
|
407
|
+
try {
|
|
408
|
+
this.db.run(`
|
|
409
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
410
|
+
id TEXT PRIMARY KEY,
|
|
411
|
+
timestamp TEXT NOT NULL,
|
|
412
|
+
name TEXT NOT NULL,
|
|
413
|
+
type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
|
|
414
|
+
value REAL NOT NULL,
|
|
415
|
+
tags_json TEXT DEFAULT '{}',
|
|
416
|
+
service TEXT NOT NULL,
|
|
417
|
+
environment TEXT
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
421
|
+
trace_id TEXT NOT NULL,
|
|
422
|
+
span_id TEXT PRIMARY KEY,
|
|
423
|
+
parent_span_id TEXT,
|
|
424
|
+
service TEXT NOT NULL,
|
|
425
|
+
symbol TEXT NOT NULL,
|
|
426
|
+
operation TEXT NOT NULL,
|
|
427
|
+
start_time TEXT NOT NULL,
|
|
428
|
+
end_time TEXT,
|
|
429
|
+
duration_ms REAL,
|
|
430
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
431
|
+
tags_json TEXT DEFAULT '{}',
|
|
432
|
+
log_ids_json TEXT DEFAULT '[]'
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
|
|
437
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
|
|
439
|
+
CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
|
|
441
|
+
`);
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
this.db.run(
|
|
445
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '4')"
|
|
446
|
+
);
|
|
447
|
+
currentVersion = 4;
|
|
448
|
+
}
|
|
449
|
+
if (currentVersion < 5) {
|
|
450
|
+
try {
|
|
451
|
+
this.db.run(`
|
|
452
|
+
CREATE TABLE IF NOT EXISTS schemas (
|
|
453
|
+
id TEXT PRIMARY KEY,
|
|
454
|
+
version TEXT NOT NULL,
|
|
455
|
+
name TEXT NOT NULL,
|
|
456
|
+
description TEXT,
|
|
457
|
+
scope_json TEXT NOT NULL,
|
|
458
|
+
event_types_json TEXT NOT NULL,
|
|
459
|
+
causality_json TEXT,
|
|
460
|
+
visualization_json TEXT,
|
|
461
|
+
tags_json TEXT DEFAULT '[]',
|
|
462
|
+
registered_at TEXT NOT NULL,
|
|
463
|
+
updated_at TEXT NOT NULL
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
467
|
+
id TEXT PRIMARY KEY,
|
|
468
|
+
schema_id TEXT NOT NULL,
|
|
469
|
+
event_type TEXT NOT NULL,
|
|
470
|
+
category TEXT NOT NULL,
|
|
471
|
+
timestamp TEXT NOT NULL,
|
|
472
|
+
scope_value TEXT,
|
|
473
|
+
scope_ordinal INTEGER,
|
|
474
|
+
session_id TEXT,
|
|
475
|
+
service TEXT NOT NULL,
|
|
476
|
+
data_json TEXT,
|
|
477
|
+
severity TEXT DEFAULT 'info',
|
|
478
|
+
parent_event_id TEXT,
|
|
479
|
+
depth INTEGER DEFAULT 0
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
CREATE INDEX IF NOT EXISTS idx_events_schema ON events(schema_id);
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
|
|
484
|
+
CREATE INDEX IF NOT EXISTS idx_events_scope ON events(schema_id, scope_value);
|
|
485
|
+
CREATE INDEX IF NOT EXISTS idx_events_scope_ord ON events(schema_id, scope_ordinal);
|
|
486
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
487
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
488
|
+
CREATE INDEX IF NOT EXISTS idx_events_service ON events(service);
|
|
489
|
+
`);
|
|
490
|
+
} catch {
|
|
491
|
+
}
|
|
492
|
+
this.db.run(
|
|
493
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '5')"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Ensure the storage is ready for use. Must be called once before using storage methods.
|
|
499
|
+
*/
|
|
500
|
+
async ensureReady() {
|
|
501
|
+
if (!SQL) {
|
|
502
|
+
SQL = await initSqlJs();
|
|
503
|
+
}
|
|
504
|
+
this.initializeSync();
|
|
505
|
+
}
|
|
506
|
+
getIncident(id) {
|
|
507
|
+
this.initializeSync();
|
|
508
|
+
const result = this.db.exec("SELECT * FROM incidents WHERE id = ?", [id]);
|
|
509
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
510
|
+
return this.rowToIncident(result[0].columns, result[0].values[0]);
|
|
511
|
+
}
|
|
512
|
+
getRecentIncidents(options = {}) {
|
|
513
|
+
this.initializeSync();
|
|
514
|
+
const { limit = 50, offset = 0 } = options;
|
|
515
|
+
const conditions = [];
|
|
516
|
+
const params = [];
|
|
517
|
+
if (options.status && options.status !== "all") {
|
|
518
|
+
conditions.push("status = ?");
|
|
519
|
+
params.push(options.status);
|
|
520
|
+
}
|
|
521
|
+
if (options.environment) {
|
|
522
|
+
conditions.push("environment = ?");
|
|
523
|
+
params.push(options.environment);
|
|
524
|
+
}
|
|
525
|
+
if (options.symbol) {
|
|
526
|
+
conditions.push("symbols LIKE ?");
|
|
527
|
+
params.push(`%${options.symbol}%`);
|
|
528
|
+
}
|
|
529
|
+
if (options.search) {
|
|
530
|
+
conditions.push("(error_message LIKE ? OR notes LIKE ?)");
|
|
531
|
+
params.push(`%${options.search}%`, `%${options.search}%`);
|
|
532
|
+
}
|
|
533
|
+
if (options.dateFrom) {
|
|
534
|
+
conditions.push("timestamp >= ?");
|
|
535
|
+
params.push(options.dateFrom);
|
|
536
|
+
}
|
|
537
|
+
if (options.dateTo) {
|
|
538
|
+
conditions.push("timestamp <= ?");
|
|
539
|
+
params.push(options.dateTo);
|
|
540
|
+
}
|
|
541
|
+
if (options.groupId) {
|
|
542
|
+
conditions.push("group_id = ?");
|
|
543
|
+
params.push(options.groupId);
|
|
544
|
+
}
|
|
545
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
546
|
+
const result = this.db.exec(
|
|
547
|
+
`SELECT * FROM incidents ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
548
|
+
[...params, limit, offset]
|
|
549
|
+
);
|
|
550
|
+
if (result.length === 0) return [];
|
|
551
|
+
return result[0].values.map(
|
|
552
|
+
(row) => this.rowToIncident(result[0].columns, row)
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
updateIncident(id, updates) {
|
|
556
|
+
this.initializeSync();
|
|
557
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
558
|
+
const setClauses = ["updated_at = ?"];
|
|
559
|
+
const params = [now];
|
|
560
|
+
if (updates.status !== void 0) {
|
|
561
|
+
setClauses.push("status = ?");
|
|
562
|
+
params.push(updates.status);
|
|
563
|
+
}
|
|
564
|
+
if (updates.error !== void 0) {
|
|
565
|
+
setClauses.push("error_message = ?");
|
|
566
|
+
params.push(updates.error.message);
|
|
567
|
+
if (updates.error.stack !== void 0) {
|
|
568
|
+
setClauses.push("error_stack = ?");
|
|
569
|
+
params.push(updates.error.stack || null);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (updates.symbols !== void 0) {
|
|
573
|
+
setClauses.push("symbols = ?");
|
|
574
|
+
params.push(JSON.stringify(updates.symbols));
|
|
575
|
+
}
|
|
576
|
+
if (updates.flowPosition !== void 0) {
|
|
577
|
+
setClauses.push("flow_position = ?");
|
|
578
|
+
params.push(
|
|
579
|
+
updates.flowPosition ? JSON.stringify(updates.flowPosition) : null
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (updates.groupId !== void 0) {
|
|
583
|
+
setClauses.push("group_id = ?");
|
|
584
|
+
params.push(updates.groupId || null);
|
|
585
|
+
}
|
|
586
|
+
if (updates.resolvedAt !== void 0) {
|
|
587
|
+
setClauses.push("resolved_at = ?");
|
|
588
|
+
params.push(updates.resolvedAt || null);
|
|
589
|
+
}
|
|
590
|
+
if (updates.resolvedBy !== void 0) {
|
|
591
|
+
setClauses.push("resolved_by = ?");
|
|
592
|
+
params.push(updates.resolvedBy || null);
|
|
593
|
+
}
|
|
594
|
+
if (updates.resolution !== void 0) {
|
|
595
|
+
setClauses.push("resolution = ?");
|
|
596
|
+
params.push(
|
|
597
|
+
updates.resolution ? JSON.stringify(updates.resolution) : null
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
params.push(id);
|
|
601
|
+
this.db.run(
|
|
602
|
+
`UPDATE incidents SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
603
|
+
params
|
|
604
|
+
);
|
|
605
|
+
this.save();
|
|
606
|
+
}
|
|
607
|
+
addIncidentNote(incidentId, note) {
|
|
608
|
+
this.initializeSync();
|
|
609
|
+
const incident = this.getIncident(incidentId);
|
|
610
|
+
if (!incident) return;
|
|
611
|
+
const newNote = {
|
|
612
|
+
id: uuidv4(),
|
|
613
|
+
...note
|
|
614
|
+
};
|
|
615
|
+
const notes = [...incident.notes, newNote];
|
|
616
|
+
this.db.run(
|
|
617
|
+
"UPDATE incidents SET notes = ?, updated_at = ? WHERE id = ?",
|
|
618
|
+
[JSON.stringify(notes), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
|
|
619
|
+
);
|
|
620
|
+
this.save();
|
|
621
|
+
}
|
|
622
|
+
linkIncidents(incidentId, relatedId) {
|
|
623
|
+
this.initializeSync();
|
|
624
|
+
const incident = this.getIncident(incidentId);
|
|
625
|
+
if (!incident) return;
|
|
626
|
+
if (!incident.relatedIncidents.includes(relatedId)) {
|
|
627
|
+
const related = [...incident.relatedIncidents, relatedId];
|
|
628
|
+
this.db.run(
|
|
629
|
+
"UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
|
|
630
|
+
[JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), incidentId]
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
const relatedIncident = this.getIncident(relatedId);
|
|
634
|
+
if (relatedIncident && !relatedIncident.relatedIncidents.includes(incidentId)) {
|
|
635
|
+
const related = [...relatedIncident.relatedIncidents, incidentId];
|
|
636
|
+
this.db.run(
|
|
637
|
+
"UPDATE incidents SET related_incidents = ?, updated_at = ? WHERE id = ?",
|
|
638
|
+
[JSON.stringify(related), (/* @__PURE__ */ new Date()).toISOString(), relatedId]
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
this.save();
|
|
642
|
+
}
|
|
643
|
+
getIncidentCount(options = {}) {
|
|
644
|
+
this.initializeSync();
|
|
645
|
+
const conditions = [];
|
|
646
|
+
const params = [];
|
|
647
|
+
if (options.status && options.status !== "all") {
|
|
648
|
+
conditions.push("status = ?");
|
|
649
|
+
params.push(options.status);
|
|
650
|
+
}
|
|
651
|
+
if (options.environment) {
|
|
652
|
+
conditions.push("environment = ?");
|
|
653
|
+
params.push(options.environment);
|
|
654
|
+
}
|
|
655
|
+
if (options.dateFrom) {
|
|
656
|
+
conditions.push("timestamp >= ?");
|
|
657
|
+
params.push(options.dateFrom);
|
|
658
|
+
}
|
|
659
|
+
if (options.dateTo) {
|
|
660
|
+
conditions.push("timestamp <= ?");
|
|
661
|
+
params.push(options.dateTo);
|
|
662
|
+
}
|
|
663
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
664
|
+
const result = this.db.exec(
|
|
665
|
+
`SELECT COUNT(*) as count FROM incidents ${whereClause}`,
|
|
666
|
+
params
|
|
667
|
+
);
|
|
668
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
669
|
+
return result[0].values[0][0];
|
|
670
|
+
}
|
|
671
|
+
// ─── Patterns ────────────────────────────────────────────────────
|
|
672
|
+
addPattern(input) {
|
|
673
|
+
this.initializeSync();
|
|
674
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
675
|
+
const confidence = {
|
|
676
|
+
...DEFAULT_CONFIDENCE,
|
|
677
|
+
...input.confidence
|
|
678
|
+
};
|
|
679
|
+
this.db.run(
|
|
680
|
+
`INSERT INTO patterns (
|
|
681
|
+
id, name, description, pattern, resolution, confidence,
|
|
682
|
+
source, private, tags, created_at, updated_at
|
|
683
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
684
|
+
[
|
|
685
|
+
input.id,
|
|
686
|
+
input.name,
|
|
687
|
+
input.description || null,
|
|
688
|
+
JSON.stringify(input.pattern),
|
|
689
|
+
JSON.stringify(input.resolution),
|
|
690
|
+
JSON.stringify(confidence),
|
|
691
|
+
input.source,
|
|
692
|
+
input.private ? 1 : 0,
|
|
693
|
+
JSON.stringify(input.tags || []),
|
|
694
|
+
now,
|
|
695
|
+
now
|
|
696
|
+
]
|
|
697
|
+
);
|
|
698
|
+
this.save();
|
|
699
|
+
return input.id;
|
|
700
|
+
}
|
|
701
|
+
getPattern(id) {
|
|
702
|
+
this.initializeSync();
|
|
703
|
+
const result = this.db.exec("SELECT * FROM patterns WHERE id = ?", [id]);
|
|
704
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
705
|
+
return this.rowToPattern(result[0].columns, result[0].values[0]);
|
|
706
|
+
}
|
|
707
|
+
getAllPatterns(options = {}) {
|
|
708
|
+
this.initializeSync();
|
|
709
|
+
const conditions = [];
|
|
710
|
+
const params = [];
|
|
711
|
+
if (options.source) {
|
|
712
|
+
conditions.push("source = ?");
|
|
713
|
+
params.push(options.source);
|
|
714
|
+
}
|
|
715
|
+
if (options.minConfidence !== void 0) {
|
|
716
|
+
conditions.push("json_extract(confidence, '$.score') >= ?");
|
|
717
|
+
params.push(options.minConfidence);
|
|
718
|
+
}
|
|
719
|
+
if (!options.includePrivate) {
|
|
720
|
+
conditions.push("private = 0");
|
|
721
|
+
}
|
|
722
|
+
if (options.tags && options.tags.length > 0) {
|
|
723
|
+
const tagConditions = options.tags.map(() => "tags LIKE ?");
|
|
724
|
+
conditions.push(`(${tagConditions.join(" OR ")})`);
|
|
725
|
+
params.push(...options.tags.map((tag) => `%"${tag}"%`));
|
|
726
|
+
}
|
|
727
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
728
|
+
const result = this.db.exec(
|
|
729
|
+
`SELECT * FROM patterns ${whereClause} ORDER BY json_extract(confidence, '$.score') DESC`,
|
|
730
|
+
params
|
|
731
|
+
);
|
|
732
|
+
if (result.length === 0) return [];
|
|
733
|
+
return result[0].values.map(
|
|
734
|
+
(row) => this.rowToPattern(result[0].columns, row)
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
updatePattern(id, updates) {
|
|
738
|
+
this.initializeSync();
|
|
739
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
740
|
+
const setClauses = ["updated_at = ?"];
|
|
741
|
+
const params = [now];
|
|
742
|
+
if (updates.name !== void 0) {
|
|
743
|
+
setClauses.push("name = ?");
|
|
744
|
+
params.push(updates.name);
|
|
745
|
+
}
|
|
746
|
+
if (updates.description !== void 0) {
|
|
747
|
+
setClauses.push("description = ?");
|
|
748
|
+
params.push(updates.description || null);
|
|
749
|
+
}
|
|
750
|
+
if (updates.pattern !== void 0) {
|
|
751
|
+
setClauses.push("pattern = ?");
|
|
752
|
+
params.push(JSON.stringify(updates.pattern));
|
|
753
|
+
}
|
|
754
|
+
if (updates.resolution !== void 0) {
|
|
755
|
+
setClauses.push("resolution = ?");
|
|
756
|
+
params.push(JSON.stringify(updates.resolution));
|
|
757
|
+
}
|
|
758
|
+
if (updates.confidence !== void 0) {
|
|
759
|
+
setClauses.push("confidence = ?");
|
|
760
|
+
params.push(JSON.stringify(updates.confidence));
|
|
761
|
+
}
|
|
762
|
+
if (updates.source !== void 0) {
|
|
763
|
+
setClauses.push("source = ?");
|
|
764
|
+
params.push(updates.source);
|
|
765
|
+
}
|
|
766
|
+
if (updates.private !== void 0) {
|
|
767
|
+
setClauses.push("private = ?");
|
|
768
|
+
params.push(updates.private ? 1 : 0);
|
|
769
|
+
}
|
|
770
|
+
if (updates.tags !== void 0) {
|
|
771
|
+
setClauses.push("tags = ?");
|
|
772
|
+
params.push(JSON.stringify(updates.tags));
|
|
773
|
+
}
|
|
774
|
+
params.push(id);
|
|
775
|
+
this.db.run(
|
|
776
|
+
`UPDATE patterns SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
777
|
+
params
|
|
778
|
+
);
|
|
779
|
+
this.save();
|
|
780
|
+
}
|
|
781
|
+
deletePattern(id) {
|
|
782
|
+
this.initializeSync();
|
|
783
|
+
this.db.run("DELETE FROM patterns WHERE id = ?", [id]);
|
|
784
|
+
this.save();
|
|
785
|
+
}
|
|
786
|
+
updatePatternConfidence(patternId, event) {
|
|
787
|
+
const pattern = this.getPattern(patternId);
|
|
788
|
+
if (!pattern) return;
|
|
789
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
790
|
+
const confidence = { ...pattern.confidence };
|
|
791
|
+
switch (event) {
|
|
792
|
+
case "matched":
|
|
793
|
+
confidence.timesMatched++;
|
|
794
|
+
confidence.lastMatched = now;
|
|
795
|
+
break;
|
|
796
|
+
case "resolved":
|
|
797
|
+
confidence.timesResolved++;
|
|
798
|
+
confidence.lastResolved = now;
|
|
799
|
+
confidence.score = Math.min(100, confidence.score + 2);
|
|
800
|
+
break;
|
|
801
|
+
case "recurred":
|
|
802
|
+
confidence.timesRecurred++;
|
|
803
|
+
confidence.score = Math.max(10, confidence.score - 5);
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
this.updatePattern(patternId, { confidence });
|
|
807
|
+
}
|
|
808
|
+
// ─── Groups ──────────────────────────────────────────────────────
|
|
809
|
+
createGroup(input) {
|
|
810
|
+
this.initializeSync();
|
|
811
|
+
const id = `GRP-${uuidv4().substring(0, 8)}`;
|
|
812
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
813
|
+
this.db.run(
|
|
814
|
+
`INSERT INTO groups (
|
|
815
|
+
id, name, common_symbols, common_error_patterns,
|
|
816
|
+
suggested_pattern_id, first_seen, last_seen, created_at, updated_at
|
|
817
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
818
|
+
[
|
|
819
|
+
id,
|
|
820
|
+
input.name || null,
|
|
821
|
+
JSON.stringify(input.commonSymbols),
|
|
822
|
+
JSON.stringify(input.commonErrorPatterns),
|
|
823
|
+
input.suggestedPattern?.id || null,
|
|
824
|
+
input.firstSeen,
|
|
825
|
+
input.lastSeen,
|
|
826
|
+
now,
|
|
827
|
+
now
|
|
828
|
+
]
|
|
829
|
+
);
|
|
830
|
+
for (const incidentId of input.incidents) {
|
|
831
|
+
this.addToGroup(id, incidentId);
|
|
832
|
+
}
|
|
833
|
+
this.save();
|
|
834
|
+
return id;
|
|
835
|
+
}
|
|
836
|
+
getGroup(id) {
|
|
837
|
+
this.initializeSync();
|
|
838
|
+
const result = this.db.exec("SELECT * FROM groups WHERE id = ?", [id]);
|
|
839
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
840
|
+
return this.rowToGroup(result[0].columns, result[0].values[0]);
|
|
841
|
+
}
|
|
842
|
+
getGroups(options = {}) {
|
|
843
|
+
this.initializeSync();
|
|
844
|
+
const limit = options.limit || 100;
|
|
845
|
+
const result = this.db.exec(
|
|
846
|
+
"SELECT * FROM groups ORDER BY last_seen DESC LIMIT ?",
|
|
847
|
+
[limit]
|
|
848
|
+
);
|
|
849
|
+
if (result.length === 0) return [];
|
|
850
|
+
return result[0].values.map(
|
|
851
|
+
(row) => this.rowToGroup(result[0].columns, row)
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
addToGroup(groupId, incidentId) {
|
|
855
|
+
this.initializeSync();
|
|
856
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
857
|
+
this.db.run(
|
|
858
|
+
"INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
|
|
859
|
+
[groupId, incidentId, now]
|
|
860
|
+
);
|
|
861
|
+
this.db.run("UPDATE incidents SET group_id = ? WHERE id = ?", [
|
|
862
|
+
groupId,
|
|
863
|
+
incidentId
|
|
864
|
+
]);
|
|
865
|
+
this.db.run(
|
|
866
|
+
"UPDATE groups SET last_seen = ?, updated_at = ? WHERE id = ?",
|
|
867
|
+
[now, now, groupId]
|
|
868
|
+
);
|
|
869
|
+
this.save();
|
|
870
|
+
}
|
|
871
|
+
// ─── Resolutions ─────────────────────────────────────────────────
|
|
872
|
+
recordResolution(resolution) {
|
|
873
|
+
this.initializeSync();
|
|
874
|
+
const id = uuidv4();
|
|
875
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
876
|
+
this.db.run(
|
|
877
|
+
`INSERT INTO resolutions (id, incident_id, pattern_id, commit_hash, pr_url, notes, resolved_at)
|
|
878
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
879
|
+
[
|
|
880
|
+
id,
|
|
881
|
+
resolution.incidentId,
|
|
882
|
+
resolution.patternId || null,
|
|
883
|
+
resolution.commitHash || null,
|
|
884
|
+
resolution.prUrl || null,
|
|
885
|
+
resolution.notes || null,
|
|
886
|
+
now
|
|
887
|
+
]
|
|
888
|
+
);
|
|
889
|
+
this.updateIncident(resolution.incidentId, {
|
|
890
|
+
status: "resolved",
|
|
891
|
+
resolvedAt: now,
|
|
892
|
+
resolvedBy: resolution.patternId || "manual",
|
|
893
|
+
resolution: {
|
|
894
|
+
patternId: resolution.patternId,
|
|
895
|
+
commitHash: resolution.commitHash,
|
|
896
|
+
prUrl: resolution.prUrl,
|
|
897
|
+
notes: resolution.notes
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
if (resolution.patternId) {
|
|
901
|
+
this.updatePatternConfidence(resolution.patternId, "resolved");
|
|
902
|
+
}
|
|
903
|
+
this.save();
|
|
904
|
+
}
|
|
905
|
+
markRecurred(incidentId) {
|
|
906
|
+
this.initializeSync();
|
|
907
|
+
this.db.run(
|
|
908
|
+
"UPDATE resolutions SET recurred = 1 WHERE incident_id = ?",
|
|
909
|
+
[incidentId]
|
|
910
|
+
);
|
|
911
|
+
const result = this.db.exec(
|
|
912
|
+
"SELECT pattern_id FROM resolutions WHERE incident_id = ?",
|
|
913
|
+
[incidentId]
|
|
914
|
+
);
|
|
915
|
+
if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
|
|
916
|
+
this.updatePatternConfidence(result[0].values[0][0], "recurred");
|
|
917
|
+
}
|
|
918
|
+
this.save();
|
|
919
|
+
}
|
|
920
|
+
getResolutionHistory(options = {}) {
|
|
921
|
+
this.initializeSync();
|
|
922
|
+
const conditions = [];
|
|
923
|
+
const params = [];
|
|
924
|
+
if (options.patternId) {
|
|
925
|
+
conditions.push("pattern_id = ?");
|
|
926
|
+
params.push(options.patternId);
|
|
927
|
+
}
|
|
928
|
+
if (options.symbol) {
|
|
929
|
+
conditions.push(`incident_id IN (
|
|
930
|
+
SELECT id FROM incidents WHERE symbols LIKE ?
|
|
931
|
+
)`);
|
|
932
|
+
params.push(`%${options.symbol}%`);
|
|
933
|
+
}
|
|
934
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
935
|
+
const limit = options.limit || 100;
|
|
936
|
+
const result = this.db.exec(
|
|
937
|
+
`SELECT * FROM resolutions ${whereClause} ORDER BY resolved_at DESC LIMIT ?`,
|
|
938
|
+
[...params, limit]
|
|
939
|
+
);
|
|
940
|
+
if (result.length === 0) return [];
|
|
941
|
+
const columns = result[0].columns;
|
|
942
|
+
return result[0].values.map((row) => {
|
|
943
|
+
const obj = {};
|
|
944
|
+
columns.forEach((col, i) => {
|
|
945
|
+
obj[col] = row[i];
|
|
946
|
+
});
|
|
947
|
+
return {
|
|
948
|
+
id: obj.id,
|
|
949
|
+
incidentId: obj.incident_id,
|
|
950
|
+
patternId: obj.pattern_id || void 0,
|
|
951
|
+
commitHash: obj.commit_hash || void 0,
|
|
952
|
+
prUrl: obj.pr_url || void 0,
|
|
953
|
+
notes: obj.notes || void 0,
|
|
954
|
+
resolvedAt: obj.resolved_at,
|
|
955
|
+
recurred: obj.recurred === 1
|
|
956
|
+
};
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
// ─── Stats ───────────────────────────────────────────────────────
|
|
960
|
+
getStats(period) {
|
|
961
|
+
this.initializeSync();
|
|
962
|
+
const { start, end } = period;
|
|
963
|
+
const total = this.getIncidentCount({ dateFrom: start, dateTo: end });
|
|
964
|
+
const open = this.getIncidentCount({
|
|
965
|
+
dateFrom: start,
|
|
966
|
+
dateTo: end,
|
|
967
|
+
status: "open"
|
|
968
|
+
});
|
|
969
|
+
const resolved = this.getIncidentCount({
|
|
970
|
+
dateFrom: start,
|
|
971
|
+
dateTo: end,
|
|
972
|
+
status: "resolved"
|
|
973
|
+
});
|
|
974
|
+
const envResult = this.db.exec(
|
|
975
|
+
`SELECT environment, COUNT(*) as count
|
|
976
|
+
FROM incidents
|
|
977
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
978
|
+
GROUP BY environment`,
|
|
979
|
+
[start, end]
|
|
980
|
+
);
|
|
981
|
+
const byEnvironment = {};
|
|
982
|
+
if (envResult.length > 0) {
|
|
983
|
+
for (const row of envResult[0].values) {
|
|
984
|
+
byEnvironment[row[0]] = row[1];
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const dayResult = this.db.exec(
|
|
988
|
+
`SELECT DATE(timestamp) as date, COUNT(*) as count
|
|
989
|
+
FROM incidents
|
|
990
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
991
|
+
GROUP BY DATE(timestamp)
|
|
992
|
+
ORDER BY date`,
|
|
993
|
+
[start, end]
|
|
994
|
+
);
|
|
995
|
+
const byDay = [];
|
|
996
|
+
if (dayResult.length > 0) {
|
|
997
|
+
for (const row of dayResult[0].values) {
|
|
998
|
+
byDay.push({ date: row[0], count: row[1] });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const patterns = this.getAllPatterns({ includePrivate: true });
|
|
1002
|
+
const avgConfidence = patterns.length > 0 ? patterns.reduce((sum, p) => sum + p.confidence.score, 0) / patterns.length : 0;
|
|
1003
|
+
const mostEffective = patterns.sort((a, b) => b.confidence.timesResolved - a.confidence.timesResolved).slice(0, 5).map((p) => ({ patternId: p.id, resolvedCount: p.confidence.timesResolved }));
|
|
1004
|
+
const leastEffective = patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
|
|
1005
|
+
patternId: p.id,
|
|
1006
|
+
recurrenceRate: p.confidence.timesRecurred / Math.max(1, p.confidence.timesResolved)
|
|
1007
|
+
})).sort((a, b) => b.recurrenceRate - a.recurrenceRate).slice(0, 5);
|
|
1008
|
+
const symbolCounts = /* @__PURE__ */ new Map();
|
|
1009
|
+
const incidents = this.getRecentIncidents({
|
|
1010
|
+
dateFrom: start,
|
|
1011
|
+
dateTo: end,
|
|
1012
|
+
limit: 1e3
|
|
1013
|
+
});
|
|
1014
|
+
for (const incident of incidents) {
|
|
1015
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
1016
|
+
if (value) {
|
|
1017
|
+
symbolCounts.set(value, (symbolCounts.get(value) || 0) + 1);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const mostIncidents = Array.from(symbolCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([symbol, count]) => ({ symbol, count }));
|
|
1022
|
+
const resolutions = this.getResolutionHistory({ limit: 1e3 });
|
|
1023
|
+
const periodResolutions = resolutions.filter(
|
|
1024
|
+
(r) => r.resolvedAt >= start && r.resolvedAt <= end
|
|
1025
|
+
);
|
|
1026
|
+
const resolvedWithPattern = periodResolutions.filter(
|
|
1027
|
+
(r) => r.patternId
|
|
1028
|
+
).length;
|
|
1029
|
+
const resolvedManually = periodResolutions.length - resolvedWithPattern;
|
|
1030
|
+
return {
|
|
1031
|
+
period: { start, end },
|
|
1032
|
+
incidents: {
|
|
1033
|
+
total,
|
|
1034
|
+
open,
|
|
1035
|
+
resolved,
|
|
1036
|
+
byEnvironment,
|
|
1037
|
+
byDay
|
|
1038
|
+
},
|
|
1039
|
+
patterns: {
|
|
1040
|
+
total: patterns.length,
|
|
1041
|
+
avgConfidence: Math.round(avgConfidence),
|
|
1042
|
+
mostEffective,
|
|
1043
|
+
leastEffective
|
|
1044
|
+
},
|
|
1045
|
+
symbols: {
|
|
1046
|
+
mostIncidents,
|
|
1047
|
+
mostResolved: [],
|
|
1048
|
+
hotspots: mostIncidents.slice(0, 5).map((s) => ({
|
|
1049
|
+
symbol: s.symbol,
|
|
1050
|
+
incidentRate: s.count / Math.max(1, total)
|
|
1051
|
+
}))
|
|
1052
|
+
},
|
|
1053
|
+
resolution: {
|
|
1054
|
+
avgTimeToResolve: 0,
|
|
1055
|
+
resolvedWithPattern,
|
|
1056
|
+
resolvedManually,
|
|
1057
|
+
resolutionRate: total > 0 ? resolved / total * 100 : 0
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
getSymbolHealth(symbol) {
|
|
1062
|
+
const incidents = this.getRecentIncidents({ symbol, limit: 1e3 });
|
|
1063
|
+
const incidentCount = incidents.length;
|
|
1064
|
+
const patternCounts = /* @__PURE__ */ new Map();
|
|
1065
|
+
for (const incident of incidents) {
|
|
1066
|
+
if (incident.resolution?.patternId) {
|
|
1067
|
+
const count = patternCounts.get(incident.resolution.patternId) || 0;
|
|
1068
|
+
patternCounts.set(incident.resolution.patternId, count + 1);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const topPatterns = Array.from(patternCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([patternId, count]) => ({ patternId, count }));
|
|
1072
|
+
return {
|
|
1073
|
+
incidentCount,
|
|
1074
|
+
avgTimeToResolve: 0,
|
|
1075
|
+
topPatterns
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
// ─── Import/Export ───────────────────────────────────────────────
|
|
1079
|
+
exportPatterns(options = {}) {
|
|
1080
|
+
const patterns = this.getAllPatterns({
|
|
1081
|
+
includePrivate: options.includePrivate
|
|
1082
|
+
});
|
|
1083
|
+
return {
|
|
1084
|
+
version: "1.0.0",
|
|
1085
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1086
|
+
patterns
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
importPatterns(data, options = {}) {
|
|
1090
|
+
let imported = 0;
|
|
1091
|
+
let skipped = 0;
|
|
1092
|
+
for (const pattern of data.patterns) {
|
|
1093
|
+
const existing = this.getPattern(pattern.id);
|
|
1094
|
+
if (existing && !options.overwrite) {
|
|
1095
|
+
skipped++;
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
if (existing) {
|
|
1099
|
+
this.updatePattern(pattern.id, pattern);
|
|
1100
|
+
} else {
|
|
1101
|
+
this.addPattern({
|
|
1102
|
+
...pattern,
|
|
1103
|
+
source: "imported"
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
imported++;
|
|
1107
|
+
}
|
|
1108
|
+
return { imported, skipped };
|
|
1109
|
+
}
|
|
1110
|
+
exportBackup() {
|
|
1111
|
+
const incidents = this.getRecentIncidents({ limit: 1e5 });
|
|
1112
|
+
const patterns = this.getAllPatterns({ includePrivate: true });
|
|
1113
|
+
const groups = this.getGroups({ limit: 1e4 });
|
|
1114
|
+
return {
|
|
1115
|
+
version: "1.0.0",
|
|
1116
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1117
|
+
incidents,
|
|
1118
|
+
patterns,
|
|
1119
|
+
groups
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
importBackup(data) {
|
|
1123
|
+
this.initializeSync();
|
|
1124
|
+
this.db.run("DELETE FROM group_members");
|
|
1125
|
+
this.db.run("DELETE FROM resolutions");
|
|
1126
|
+
this.db.run("DELETE FROM groups");
|
|
1127
|
+
this.db.run("DELETE FROM incidents");
|
|
1128
|
+
this.db.run("DELETE FROM patterns");
|
|
1129
|
+
for (const pattern of data.patterns) {
|
|
1130
|
+
this.addPattern(pattern);
|
|
1131
|
+
}
|
|
1132
|
+
for (const incident of data.incidents) {
|
|
1133
|
+
const now2 = incident.timestamp;
|
|
1134
|
+
this.db.run(
|
|
1135
|
+
`INSERT INTO incidents (
|
|
1136
|
+
id, timestamp, status, error_message, error_stack, error_code, error_type,
|
|
1137
|
+
symbols, flow_position, environment, service, version, user_id, request_id,
|
|
1138
|
+
group_id, notes, related_incidents, resolved_at, resolved_by, resolution,
|
|
1139
|
+
created_at, updated_at
|
|
1140
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1141
|
+
[
|
|
1142
|
+
incident.id,
|
|
1143
|
+
incident.timestamp,
|
|
1144
|
+
incident.status,
|
|
1145
|
+
incident.error.message,
|
|
1146
|
+
incident.error.stack || null,
|
|
1147
|
+
incident.error.code || null,
|
|
1148
|
+
incident.error.type || null,
|
|
1149
|
+
JSON.stringify(incident.symbols),
|
|
1150
|
+
incident.flowPosition ? JSON.stringify(incident.flowPosition) : null,
|
|
1151
|
+
incident.environment,
|
|
1152
|
+
incident.service || null,
|
|
1153
|
+
incident.version || null,
|
|
1154
|
+
incident.userId || null,
|
|
1155
|
+
incident.requestId || null,
|
|
1156
|
+
incident.groupId || null,
|
|
1157
|
+
JSON.stringify(incident.notes),
|
|
1158
|
+
JSON.stringify(incident.relatedIncidents),
|
|
1159
|
+
incident.resolvedAt || null,
|
|
1160
|
+
incident.resolvedBy || null,
|
|
1161
|
+
incident.resolution ? JSON.stringify(incident.resolution) : null,
|
|
1162
|
+
now2,
|
|
1163
|
+
now2
|
|
1164
|
+
]
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const result = this.db.exec(
|
|
1168
|
+
"SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max FROM incidents"
|
|
1169
|
+
);
|
|
1170
|
+
if (result.length > 0 && result[0].values.length > 0 && result[0].values[0][0]) {
|
|
1171
|
+
this.incidentCounter = result[0].values[0][0];
|
|
1172
|
+
}
|
|
1173
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1174
|
+
for (const group of data.groups) {
|
|
1175
|
+
this.db.run(
|
|
1176
|
+
`INSERT INTO groups (
|
|
1177
|
+
id, name, common_symbols, common_error_patterns,
|
|
1178
|
+
suggested_pattern_id, first_seen, last_seen, created_at, updated_at
|
|
1179
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1180
|
+
[
|
|
1181
|
+
group.id,
|
|
1182
|
+
group.name || null,
|
|
1183
|
+
JSON.stringify(group.commonSymbols),
|
|
1184
|
+
JSON.stringify(group.commonErrorPatterns),
|
|
1185
|
+
group.suggestedPattern?.id || null,
|
|
1186
|
+
group.firstSeen,
|
|
1187
|
+
group.lastSeen,
|
|
1188
|
+
now,
|
|
1189
|
+
now
|
|
1190
|
+
]
|
|
1191
|
+
);
|
|
1192
|
+
for (const incidentId of group.incidents) {
|
|
1193
|
+
this.db.run(
|
|
1194
|
+
"INSERT OR IGNORE INTO group_members (group_id, incident_id, added_at) VALUES (?, ?, ?)",
|
|
1195
|
+
[group.id, incidentId, now]
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
this.save();
|
|
1200
|
+
}
|
|
1201
|
+
// ─── Helper Methods ──────────────────────────────────────────────
|
|
1202
|
+
rowToIncident(columns, row) {
|
|
1203
|
+
const obj = {};
|
|
1204
|
+
columns.forEach((col, i) => {
|
|
1205
|
+
obj[col] = row[i];
|
|
1206
|
+
});
|
|
1207
|
+
return {
|
|
1208
|
+
id: obj.id,
|
|
1209
|
+
timestamp: obj.timestamp,
|
|
1210
|
+
status: obj.status,
|
|
1211
|
+
error: {
|
|
1212
|
+
message: obj.error_message,
|
|
1213
|
+
stack: obj.error_stack || void 0,
|
|
1214
|
+
code: obj.error_code || void 0,
|
|
1215
|
+
type: obj.error_type || void 0
|
|
1216
|
+
},
|
|
1217
|
+
symbols: JSON.parse(obj.symbols || "{}"),
|
|
1218
|
+
flowPosition: obj.flow_position ? JSON.parse(obj.flow_position) : void 0,
|
|
1219
|
+
environment: obj.environment,
|
|
1220
|
+
service: obj.service || void 0,
|
|
1221
|
+
version: obj.version || void 0,
|
|
1222
|
+
userId: obj.user_id || void 0,
|
|
1223
|
+
requestId: obj.request_id || void 0,
|
|
1224
|
+
groupId: obj.group_id || void 0,
|
|
1225
|
+
notes: JSON.parse(obj.notes || "[]"),
|
|
1226
|
+
relatedIncidents: JSON.parse(obj.related_incidents || "[]"),
|
|
1227
|
+
resolvedAt: obj.resolved_at || void 0,
|
|
1228
|
+
resolvedBy: obj.resolved_by || void 0,
|
|
1229
|
+
resolution: obj.resolution ? JSON.parse(obj.resolution) : void 0
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
rowToPattern(columns, row) {
|
|
1233
|
+
const obj = {};
|
|
1234
|
+
columns.forEach((col, i) => {
|
|
1235
|
+
obj[col] = row[i];
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
id: obj.id,
|
|
1239
|
+
name: obj.name,
|
|
1240
|
+
description: obj.description || "",
|
|
1241
|
+
pattern: JSON.parse(obj.pattern),
|
|
1242
|
+
resolution: JSON.parse(obj.resolution),
|
|
1243
|
+
confidence: JSON.parse(obj.confidence),
|
|
1244
|
+
source: obj.source,
|
|
1245
|
+
private: obj.private === 1,
|
|
1246
|
+
tags: JSON.parse(obj.tags || "[]"),
|
|
1247
|
+
createdAt: obj.created_at,
|
|
1248
|
+
updatedAt: obj.updated_at
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
rowToGroup(columns, row) {
|
|
1252
|
+
const obj = {};
|
|
1253
|
+
columns.forEach((col, i) => {
|
|
1254
|
+
obj[col] = row[i];
|
|
1255
|
+
});
|
|
1256
|
+
const membersResult = this.db.exec(
|
|
1257
|
+
"SELECT incident_id FROM group_members WHERE group_id = ?",
|
|
1258
|
+
[obj.id]
|
|
1259
|
+
);
|
|
1260
|
+
const incidents = [];
|
|
1261
|
+
if (membersResult.length > 0) {
|
|
1262
|
+
for (const r of membersResult[0].values) {
|
|
1263
|
+
incidents.push(r[0]);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
const envResult = this.db.exec(
|
|
1267
|
+
`SELECT DISTINCT environment FROM incidents
|
|
1268
|
+
WHERE id IN (SELECT incident_id FROM group_members WHERE group_id = ?)`,
|
|
1269
|
+
[obj.id]
|
|
1270
|
+
);
|
|
1271
|
+
const environments = [];
|
|
1272
|
+
if (envResult.length > 0) {
|
|
1273
|
+
for (const r of envResult[0].values) {
|
|
1274
|
+
environments.push(r[0]);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
id: obj.id,
|
|
1279
|
+
name: obj.name || void 0,
|
|
1280
|
+
incidents,
|
|
1281
|
+
commonSymbols: JSON.parse(obj.common_symbols || "{}"),
|
|
1282
|
+
commonErrorPatterns: JSON.parse(
|
|
1283
|
+
obj.common_error_patterns || "[]"
|
|
1284
|
+
),
|
|
1285
|
+
count: incidents.length,
|
|
1286
|
+
firstSeen: obj.first_seen,
|
|
1287
|
+
lastSeen: obj.last_seen,
|
|
1288
|
+
environments,
|
|
1289
|
+
suggestedPattern: obj.suggested_pattern_id ? this.getPattern(obj.suggested_pattern_id) || void 0 : void 0
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
// ─── Practice Events ─────────────────────────────────────────────
|
|
1293
|
+
recordPracticeEvent(input) {
|
|
1294
|
+
this.initializeSync();
|
|
1295
|
+
const id = `PE-${uuidv4().substring(0, 8)}`;
|
|
1296
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1297
|
+
this.db.run(
|
|
1298
|
+
`INSERT INTO practice_events (
|
|
1299
|
+
id, timestamp, habit_id, habit_category, result,
|
|
1300
|
+
engineer, session_id, lore_entry_id, task_description,
|
|
1301
|
+
symbols_touched, files_modified, related_incident_id, notes
|
|
1302
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1303
|
+
[
|
|
1304
|
+
id,
|
|
1305
|
+
now,
|
|
1306
|
+
input.habitId,
|
|
1307
|
+
input.habitCategory,
|
|
1308
|
+
input.result,
|
|
1309
|
+
input.engineer,
|
|
1310
|
+
input.sessionId,
|
|
1311
|
+
input.loreEntryId || null,
|
|
1312
|
+
input.taskDescription || null,
|
|
1313
|
+
JSON.stringify(input.symbolsTouched || []),
|
|
1314
|
+
JSON.stringify(input.filesModified || []),
|
|
1315
|
+
input.relatedIncidentId || null,
|
|
1316
|
+
input.notes || null
|
|
1317
|
+
]
|
|
1318
|
+
);
|
|
1319
|
+
this.save();
|
|
1320
|
+
return id;
|
|
1321
|
+
}
|
|
1322
|
+
getPracticeEvents(options = {}) {
|
|
1323
|
+
this.initializeSync();
|
|
1324
|
+
const { limit = 100, offset = 0 } = options;
|
|
1325
|
+
const conditions = [];
|
|
1326
|
+
const params = [];
|
|
1327
|
+
if (options.habitId) {
|
|
1328
|
+
conditions.push("habit_id = ?");
|
|
1329
|
+
params.push(options.habitId);
|
|
1330
|
+
}
|
|
1331
|
+
if (options.habitCategory) {
|
|
1332
|
+
conditions.push("habit_category = ?");
|
|
1333
|
+
params.push(options.habitCategory);
|
|
1334
|
+
}
|
|
1335
|
+
if (options.result) {
|
|
1336
|
+
conditions.push("result = ?");
|
|
1337
|
+
params.push(options.result);
|
|
1338
|
+
}
|
|
1339
|
+
if (options.engineer) {
|
|
1340
|
+
conditions.push("engineer = ?");
|
|
1341
|
+
params.push(options.engineer);
|
|
1342
|
+
}
|
|
1343
|
+
if (options.sessionId) {
|
|
1344
|
+
conditions.push("session_id = ?");
|
|
1345
|
+
params.push(options.sessionId);
|
|
1346
|
+
}
|
|
1347
|
+
if (options.dateFrom) {
|
|
1348
|
+
conditions.push("timestamp >= ?");
|
|
1349
|
+
params.push(options.dateFrom);
|
|
1350
|
+
}
|
|
1351
|
+
if (options.dateTo) {
|
|
1352
|
+
conditions.push("timestamp <= ?");
|
|
1353
|
+
params.push(options.dateTo);
|
|
1354
|
+
}
|
|
1355
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1356
|
+
const result = this.db.exec(
|
|
1357
|
+
`SELECT * FROM practice_events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1358
|
+
[...params, limit, offset]
|
|
1359
|
+
);
|
|
1360
|
+
if (result.length === 0) return [];
|
|
1361
|
+
return result[0].values.map(
|
|
1362
|
+
(row) => this.rowToPracticeEvent(result[0].columns, row)
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
getPracticeEventCount(options = {}) {
|
|
1366
|
+
this.initializeSync();
|
|
1367
|
+
const conditions = [];
|
|
1368
|
+
const params = [];
|
|
1369
|
+
if (options.habitId) {
|
|
1370
|
+
conditions.push("habit_id = ?");
|
|
1371
|
+
params.push(options.habitId);
|
|
1372
|
+
}
|
|
1373
|
+
if (options.habitCategory) {
|
|
1374
|
+
conditions.push("habit_category = ?");
|
|
1375
|
+
params.push(options.habitCategory);
|
|
1376
|
+
}
|
|
1377
|
+
if (options.result) {
|
|
1378
|
+
conditions.push("result = ?");
|
|
1379
|
+
params.push(options.result);
|
|
1380
|
+
}
|
|
1381
|
+
if (options.engineer) {
|
|
1382
|
+
conditions.push("engineer = ?");
|
|
1383
|
+
params.push(options.engineer);
|
|
1384
|
+
}
|
|
1385
|
+
if (options.dateFrom) {
|
|
1386
|
+
conditions.push("timestamp >= ?");
|
|
1387
|
+
params.push(options.dateFrom);
|
|
1388
|
+
}
|
|
1389
|
+
if (options.dateTo) {
|
|
1390
|
+
conditions.push("timestamp <= ?");
|
|
1391
|
+
params.push(options.dateTo);
|
|
1392
|
+
}
|
|
1393
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1394
|
+
const result = this.db.exec(
|
|
1395
|
+
`SELECT COUNT(*) as count FROM practice_events ${whereClause}`,
|
|
1396
|
+
params
|
|
1397
|
+
);
|
|
1398
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1399
|
+
return result[0].values[0][0];
|
|
1400
|
+
}
|
|
1401
|
+
getComplianceRate(options = {}) {
|
|
1402
|
+
this.initializeSync();
|
|
1403
|
+
const conditions = [];
|
|
1404
|
+
const params = [];
|
|
1405
|
+
if (options.habitId) {
|
|
1406
|
+
conditions.push("habit_id = ?");
|
|
1407
|
+
params.push(options.habitId);
|
|
1408
|
+
}
|
|
1409
|
+
if (options.habitCategory) {
|
|
1410
|
+
conditions.push("habit_category = ?");
|
|
1411
|
+
params.push(options.habitCategory);
|
|
1412
|
+
}
|
|
1413
|
+
if (options.engineer) {
|
|
1414
|
+
conditions.push("engineer = ?");
|
|
1415
|
+
params.push(options.engineer);
|
|
1416
|
+
}
|
|
1417
|
+
if (options.dateFrom) {
|
|
1418
|
+
conditions.push("timestamp >= ?");
|
|
1419
|
+
params.push(options.dateFrom);
|
|
1420
|
+
}
|
|
1421
|
+
if (options.dateTo) {
|
|
1422
|
+
conditions.push("timestamp <= ?");
|
|
1423
|
+
params.push(options.dateTo);
|
|
1424
|
+
}
|
|
1425
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1426
|
+
const result = this.db.exec(
|
|
1427
|
+
`SELECT result, COUNT(*) as count
|
|
1428
|
+
FROM practice_events ${whereClause}
|
|
1429
|
+
GROUP BY result`,
|
|
1430
|
+
params
|
|
1431
|
+
);
|
|
1432
|
+
let followed = 0;
|
|
1433
|
+
let skipped = 0;
|
|
1434
|
+
let partial = 0;
|
|
1435
|
+
if (result.length > 0) {
|
|
1436
|
+
for (const row of result[0].values) {
|
|
1437
|
+
const r = row[0];
|
|
1438
|
+
const count = row[1];
|
|
1439
|
+
if (r === "followed") followed = count;
|
|
1440
|
+
else if (r === "skipped") skipped = count;
|
|
1441
|
+
else if (r === "partial") partial = count;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
const total = followed + skipped + partial;
|
|
1445
|
+
const rate = total > 0 ? (followed + partial * 0.5) / total * 100 : 100;
|
|
1446
|
+
return { total, followed, skipped, partial, rate: Math.round(rate) };
|
|
1447
|
+
}
|
|
1448
|
+
rowToPracticeEvent(columns, row) {
|
|
1449
|
+
const obj = {};
|
|
1450
|
+
columns.forEach((col, i) => {
|
|
1451
|
+
obj[col] = row[i];
|
|
1452
|
+
});
|
|
1453
|
+
return {
|
|
1454
|
+
id: obj.id,
|
|
1455
|
+
timestamp: obj.timestamp,
|
|
1456
|
+
habitId: obj.habit_id,
|
|
1457
|
+
habitCategory: obj.habit_category,
|
|
1458
|
+
result: obj.result,
|
|
1459
|
+
engineer: obj.engineer,
|
|
1460
|
+
sessionId: obj.session_id,
|
|
1461
|
+
loreEntryId: obj.lore_entry_id || void 0,
|
|
1462
|
+
taskDescription: obj.task_description || void 0,
|
|
1463
|
+
symbolsTouched: JSON.parse(obj.symbols_touched || "[]"),
|
|
1464
|
+
filesModified: JSON.parse(obj.files_modified || "[]"),
|
|
1465
|
+
relatedIncidentId: obj.related_incident_id || void 0,
|
|
1466
|
+
notes: obj.notes || void 0
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
// ─── Structured Logs ─────────────────────────────────────────────
|
|
1470
|
+
inferSymbolType(symbol) {
|
|
1471
|
+
if (symbol.startsWith("#")) return "component";
|
|
1472
|
+
if (symbol.startsWith("^")) return "gate";
|
|
1473
|
+
if (symbol.startsWith("!")) return "signal";
|
|
1474
|
+
if (symbol.startsWith("$")) return "flow";
|
|
1475
|
+
if (symbol.startsWith("~")) return "aspect";
|
|
1476
|
+
return "raw";
|
|
1477
|
+
}
|
|
1478
|
+
insertLog(input) {
|
|
1479
|
+
this.initializeSync();
|
|
1480
|
+
const id = input.id || uuidv4();
|
|
1481
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1482
|
+
const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
|
|
1483
|
+
this.db.run(
|
|
1484
|
+
`INSERT INTO logs (
|
|
1485
|
+
id, timestamp, level, symbol, symbol_type, message, data_json,
|
|
1486
|
+
service, session_id, correlation_id, duration_ms, environment
|
|
1487
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1488
|
+
[
|
|
1489
|
+
id,
|
|
1490
|
+
timestamp,
|
|
1491
|
+
input.level,
|
|
1492
|
+
input.symbol,
|
|
1493
|
+
symbolType,
|
|
1494
|
+
input.message,
|
|
1495
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
1496
|
+
input.service,
|
|
1497
|
+
input.sessionId || null,
|
|
1498
|
+
input.correlationId || null,
|
|
1499
|
+
input.durationMs ?? null,
|
|
1500
|
+
input.environment || null
|
|
1501
|
+
]
|
|
1502
|
+
);
|
|
1503
|
+
this.save();
|
|
1504
|
+
return id;
|
|
1505
|
+
}
|
|
1506
|
+
insertLogBatch(entries) {
|
|
1507
|
+
this.initializeSync();
|
|
1508
|
+
let accepted = 0;
|
|
1509
|
+
const errors = [];
|
|
1510
|
+
for (const input of entries) {
|
|
1511
|
+
try {
|
|
1512
|
+
const id = input.id || uuidv4();
|
|
1513
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1514
|
+
const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
|
|
1515
|
+
this.db.run(
|
|
1516
|
+
`INSERT INTO logs (
|
|
1517
|
+
id, timestamp, level, symbol, symbol_type, message, data_json,
|
|
1518
|
+
service, session_id, correlation_id, duration_ms, environment
|
|
1519
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1520
|
+
[
|
|
1521
|
+
id,
|
|
1522
|
+
timestamp,
|
|
1523
|
+
input.level,
|
|
1524
|
+
input.symbol,
|
|
1525
|
+
symbolType,
|
|
1526
|
+
input.message,
|
|
1527
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
1528
|
+
input.service,
|
|
1529
|
+
input.sessionId || null,
|
|
1530
|
+
input.correlationId || null,
|
|
1531
|
+
input.durationMs ?? null,
|
|
1532
|
+
input.environment || null
|
|
1533
|
+
]
|
|
1534
|
+
);
|
|
1535
|
+
accepted++;
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
this.save();
|
|
1541
|
+
return { accepted, errors };
|
|
1542
|
+
}
|
|
1543
|
+
queryLogs(options = {}) {
|
|
1544
|
+
this.initializeSync();
|
|
1545
|
+
const { limit = 100, offset = 0 } = options;
|
|
1546
|
+
const conditions = [];
|
|
1547
|
+
const params = [];
|
|
1548
|
+
if (options.level) {
|
|
1549
|
+
conditions.push("level = ?");
|
|
1550
|
+
params.push(options.level);
|
|
1551
|
+
}
|
|
1552
|
+
if (options.symbol) {
|
|
1553
|
+
conditions.push("symbol LIKE ?");
|
|
1554
|
+
params.push(`%${options.symbol}%`);
|
|
1555
|
+
}
|
|
1556
|
+
if (options.service) {
|
|
1557
|
+
conditions.push("service = ?");
|
|
1558
|
+
params.push(options.service);
|
|
1559
|
+
}
|
|
1560
|
+
if (options.sessionId) {
|
|
1561
|
+
conditions.push("session_id = ?");
|
|
1562
|
+
params.push(options.sessionId);
|
|
1563
|
+
}
|
|
1564
|
+
if (options.correlationId) {
|
|
1565
|
+
conditions.push("correlation_id = ?");
|
|
1566
|
+
params.push(options.correlationId);
|
|
1567
|
+
}
|
|
1568
|
+
if (options.search) {
|
|
1569
|
+
conditions.push("message LIKE ?");
|
|
1570
|
+
params.push(`%${options.search}%`);
|
|
1571
|
+
}
|
|
1572
|
+
if (options.since) {
|
|
1573
|
+
conditions.push("timestamp >= ?");
|
|
1574
|
+
params.push(options.since);
|
|
1575
|
+
}
|
|
1576
|
+
if (options.until) {
|
|
1577
|
+
conditions.push("timestamp <= ?");
|
|
1578
|
+
params.push(options.until);
|
|
1579
|
+
}
|
|
1580
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1581
|
+
const result = this.db.exec(
|
|
1582
|
+
`SELECT * FROM logs ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1583
|
+
[...params, limit, offset]
|
|
1584
|
+
);
|
|
1585
|
+
if (result.length === 0) return [];
|
|
1586
|
+
return result[0].values.map(
|
|
1587
|
+
(row) => this.rowToLogEntry(result[0].columns, row)
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
getLogCount(options = {}) {
|
|
1591
|
+
this.initializeSync();
|
|
1592
|
+
const conditions = [];
|
|
1593
|
+
const params = [];
|
|
1594
|
+
if (options.level) {
|
|
1595
|
+
conditions.push("level = ?");
|
|
1596
|
+
params.push(options.level);
|
|
1597
|
+
}
|
|
1598
|
+
if (options.symbol) {
|
|
1599
|
+
conditions.push("symbol LIKE ?");
|
|
1600
|
+
params.push(`%${options.symbol}%`);
|
|
1601
|
+
}
|
|
1602
|
+
if (options.service) {
|
|
1603
|
+
conditions.push("service = ?");
|
|
1604
|
+
params.push(options.service);
|
|
1605
|
+
}
|
|
1606
|
+
if (options.since) {
|
|
1607
|
+
conditions.push("timestamp >= ?");
|
|
1608
|
+
params.push(options.since);
|
|
1609
|
+
}
|
|
1610
|
+
if (options.until) {
|
|
1611
|
+
conditions.push("timestamp <= ?");
|
|
1612
|
+
params.push(options.until);
|
|
1613
|
+
}
|
|
1614
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1615
|
+
const result = this.db.exec(
|
|
1616
|
+
`SELECT COUNT(*) as count FROM logs ${whereClause}`,
|
|
1617
|
+
params
|
|
1618
|
+
);
|
|
1619
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1620
|
+
return result[0].values[0][0];
|
|
1621
|
+
}
|
|
1622
|
+
pruneLogs(maxCount) {
|
|
1623
|
+
this.initializeSync();
|
|
1624
|
+
if (maxCount <= 0) return 0;
|
|
1625
|
+
const currentCount = this.getLogCount();
|
|
1626
|
+
if (currentCount <= maxCount) return 0;
|
|
1627
|
+
const deleteCount = currentCount - maxCount;
|
|
1628
|
+
this.db.run(
|
|
1629
|
+
`DELETE FROM logs WHERE id IN (
|
|
1630
|
+
SELECT id FROM logs ORDER BY timestamp ASC LIMIT ?
|
|
1631
|
+
)`,
|
|
1632
|
+
[deleteCount]
|
|
1633
|
+
);
|
|
1634
|
+
this.save();
|
|
1635
|
+
return deleteCount;
|
|
1636
|
+
}
|
|
1637
|
+
rowToLogEntry(columns, row) {
|
|
1638
|
+
const obj = {};
|
|
1639
|
+
columns.forEach((col, i) => {
|
|
1640
|
+
obj[col] = row[i];
|
|
1641
|
+
});
|
|
1642
|
+
return {
|
|
1643
|
+
id: obj.id,
|
|
1644
|
+
timestamp: obj.timestamp,
|
|
1645
|
+
level: obj.level,
|
|
1646
|
+
symbol: obj.symbol,
|
|
1647
|
+
symbolType: obj.symbol_type || "raw",
|
|
1648
|
+
message: obj.message,
|
|
1649
|
+
data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
|
|
1650
|
+
service: obj.service,
|
|
1651
|
+
sessionId: obj.session_id || void 0,
|
|
1652
|
+
correlationId: obj.correlation_id || void 0,
|
|
1653
|
+
durationMs: obj.duration_ms || void 0,
|
|
1654
|
+
environment: obj.environment || void 0
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
// ─── Service Registry ──────────────────────────────────────────
|
|
1658
|
+
registerService(reg) {
|
|
1659
|
+
this.initializeSync();
|
|
1660
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1661
|
+
this.db.run(
|
|
1662
|
+
`INSERT INTO services (name, version, pid, started_at, last_seen_at, environment, metadata_json)
|
|
1663
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1664
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
1665
|
+
version = excluded.version,
|
|
1666
|
+
pid = excluded.pid,
|
|
1667
|
+
last_seen_at = excluded.last_seen_at,
|
|
1668
|
+
environment = excluded.environment,
|
|
1669
|
+
metadata_json = excluded.metadata_json`,
|
|
1670
|
+
[
|
|
1671
|
+
reg.name,
|
|
1672
|
+
reg.version || null,
|
|
1673
|
+
reg.pid ?? null,
|
|
1674
|
+
now,
|
|
1675
|
+
now,
|
|
1676
|
+
reg.environment || null,
|
|
1677
|
+
reg.metadata ? JSON.stringify(reg.metadata) : null
|
|
1678
|
+
]
|
|
1679
|
+
);
|
|
1680
|
+
this.save();
|
|
1681
|
+
}
|
|
1682
|
+
updateServiceLastSeen(name) {
|
|
1683
|
+
this.initializeSync();
|
|
1684
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1685
|
+
this.db.run(
|
|
1686
|
+
"UPDATE services SET last_seen_at = ? WHERE name = ?",
|
|
1687
|
+
[now, name]
|
|
1688
|
+
);
|
|
1689
|
+
this.save();
|
|
1690
|
+
}
|
|
1691
|
+
getServices() {
|
|
1692
|
+
this.initializeSync();
|
|
1693
|
+
const result = this.db.exec(
|
|
1694
|
+
"SELECT * FROM services ORDER BY last_seen_at DESC"
|
|
1695
|
+
);
|
|
1696
|
+
if (result.length === 0) return [];
|
|
1697
|
+
return result[0].values.map((row) => {
|
|
1698
|
+
const obj = {};
|
|
1699
|
+
result[0].columns.forEach((col, i) => {
|
|
1700
|
+
obj[col] = row[i];
|
|
1701
|
+
});
|
|
1702
|
+
return {
|
|
1703
|
+
name: obj.name,
|
|
1704
|
+
version: obj.version || void 0,
|
|
1705
|
+
pid: obj.pid || void 0,
|
|
1706
|
+
startedAt: obj.started_at,
|
|
1707
|
+
lastSeenAt: obj.last_seen_at,
|
|
1708
|
+
environment: obj.environment || void 0,
|
|
1709
|
+
metadata: obj.metadata_json ? JSON.parse(obj.metadata_json) : void 0
|
|
1710
|
+
};
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
// ─── App State ──────────────────────────────────────────────────
|
|
1714
|
+
upsertAppState(state) {
|
|
1715
|
+
this.initializeSync();
|
|
1716
|
+
this.db.run(
|
|
1717
|
+
`INSERT INTO app_state (service, session_id, timestamp, state_json, active_flows_json, active_gates_json)
|
|
1718
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1719
|
+
ON CONFLICT(service, session_id) DO UPDATE SET
|
|
1720
|
+
timestamp = excluded.timestamp,
|
|
1721
|
+
state_json = excluded.state_json,
|
|
1722
|
+
active_flows_json = excluded.active_flows_json,
|
|
1723
|
+
active_gates_json = excluded.active_gates_json`,
|
|
1724
|
+
[
|
|
1725
|
+
state.service,
|
|
1726
|
+
state.sessionId,
|
|
1727
|
+
state.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1728
|
+
JSON.stringify(state.state),
|
|
1729
|
+
state.activeFlows ? JSON.stringify(state.activeFlows) : null,
|
|
1730
|
+
state.activeGates ? JSON.stringify(state.activeGates) : null
|
|
1731
|
+
]
|
|
1732
|
+
);
|
|
1733
|
+
this.save();
|
|
1734
|
+
}
|
|
1735
|
+
getAppState(service, sessionId) {
|
|
1736
|
+
this.initializeSync();
|
|
1737
|
+
let query = "SELECT * FROM app_state WHERE service = ?";
|
|
1738
|
+
const params = [service];
|
|
1739
|
+
if (sessionId) {
|
|
1740
|
+
query += " AND session_id = ?";
|
|
1741
|
+
params.push(sessionId);
|
|
1742
|
+
}
|
|
1743
|
+
query += " ORDER BY timestamp DESC";
|
|
1744
|
+
const result = this.db.exec(query, params);
|
|
1745
|
+
if (result.length === 0) return [];
|
|
1746
|
+
return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
|
|
1747
|
+
}
|
|
1748
|
+
getAllAppStates() {
|
|
1749
|
+
this.initializeSync();
|
|
1750
|
+
const result = this.db.exec(
|
|
1751
|
+
"SELECT * FROM app_state ORDER BY timestamp DESC"
|
|
1752
|
+
);
|
|
1753
|
+
if (result.length === 0) return [];
|
|
1754
|
+
return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
|
|
1755
|
+
}
|
|
1756
|
+
rowToAppState(columns, row) {
|
|
1757
|
+
const obj = {};
|
|
1758
|
+
columns.forEach((col, i) => {
|
|
1759
|
+
obj[col] = row[i];
|
|
1760
|
+
});
|
|
1761
|
+
return {
|
|
1762
|
+
service: obj.service,
|
|
1763
|
+
sessionId: obj.session_id,
|
|
1764
|
+
timestamp: obj.timestamp,
|
|
1765
|
+
state: JSON.parse(obj.state_json),
|
|
1766
|
+
activeFlows: obj.active_flows_json ? JSON.parse(obj.active_flows_json) : void 0,
|
|
1767
|
+
activeGates: obj.active_gates_json ? JSON.parse(obj.active_gates_json) : void 0
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
// ─── Metrics ───────────────────────────────────────────────────
|
|
1771
|
+
insertMetric(input) {
|
|
1772
|
+
this.initializeSync();
|
|
1773
|
+
const id = uuidv4();
|
|
1774
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1775
|
+
this.db.run(
|
|
1776
|
+
`INSERT INTO metrics (
|
|
1777
|
+
id, timestamp, name, type, value, tags_json, service, environment
|
|
1778
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1779
|
+
[
|
|
1780
|
+
id,
|
|
1781
|
+
timestamp,
|
|
1782
|
+
input.name,
|
|
1783
|
+
input.type,
|
|
1784
|
+
input.value,
|
|
1785
|
+
JSON.stringify(input.tags || {}),
|
|
1786
|
+
input.service,
|
|
1787
|
+
input.environment || null
|
|
1788
|
+
]
|
|
1789
|
+
);
|
|
1790
|
+
this.save();
|
|
1791
|
+
return id;
|
|
1792
|
+
}
|
|
1793
|
+
insertMetricBatch(entries) {
|
|
1794
|
+
this.initializeSync();
|
|
1795
|
+
let accepted = 0;
|
|
1796
|
+
const errors = [];
|
|
1797
|
+
for (const input of entries) {
|
|
1798
|
+
try {
|
|
1799
|
+
const id = uuidv4();
|
|
1800
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
1801
|
+
this.db.run(
|
|
1802
|
+
`INSERT INTO metrics (
|
|
1803
|
+
id, timestamp, name, type, value, tags_json, service, environment
|
|
1804
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1805
|
+
[
|
|
1806
|
+
id,
|
|
1807
|
+
timestamp,
|
|
1808
|
+
input.name,
|
|
1809
|
+
input.type,
|
|
1810
|
+
input.value,
|
|
1811
|
+
JSON.stringify(input.tags || {}),
|
|
1812
|
+
input.service,
|
|
1813
|
+
input.environment || null
|
|
1814
|
+
]
|
|
1815
|
+
);
|
|
1816
|
+
accepted++;
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
this.save();
|
|
1822
|
+
return { accepted, errors };
|
|
1823
|
+
}
|
|
1824
|
+
queryMetrics(options = {}) {
|
|
1825
|
+
this.initializeSync();
|
|
1826
|
+
const { limit = 100, offset = 0 } = options;
|
|
1827
|
+
const conditions = [];
|
|
1828
|
+
const params = [];
|
|
1829
|
+
if (options.name) {
|
|
1830
|
+
conditions.push("name = ?");
|
|
1831
|
+
params.push(options.name);
|
|
1832
|
+
}
|
|
1833
|
+
if (options.type) {
|
|
1834
|
+
conditions.push("type = ?");
|
|
1835
|
+
params.push(options.type);
|
|
1836
|
+
}
|
|
1837
|
+
if (options.service) {
|
|
1838
|
+
conditions.push("service = ?");
|
|
1839
|
+
params.push(options.service);
|
|
1840
|
+
}
|
|
1841
|
+
if (options.tag) {
|
|
1842
|
+
const eqIdx = options.tag.indexOf("=");
|
|
1843
|
+
if (eqIdx > 0) {
|
|
1844
|
+
const tagKey = options.tag.substring(0, eqIdx);
|
|
1845
|
+
const tagValue = options.tag.substring(eqIdx + 1);
|
|
1846
|
+
conditions.push("tags_json LIKE ?");
|
|
1847
|
+
params.push(`%"${tagKey}":"${tagValue}"%`);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if (options.since) {
|
|
1851
|
+
conditions.push("timestamp >= ?");
|
|
1852
|
+
params.push(options.since);
|
|
1853
|
+
}
|
|
1854
|
+
if (options.until) {
|
|
1855
|
+
conditions.push("timestamp <= ?");
|
|
1856
|
+
params.push(options.until);
|
|
1857
|
+
}
|
|
1858
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1859
|
+
const result = this.db.exec(
|
|
1860
|
+
`SELECT * FROM metrics ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
1861
|
+
[...params, limit, offset]
|
|
1862
|
+
);
|
|
1863
|
+
if (result.length === 0) return [];
|
|
1864
|
+
return result[0].values.map(
|
|
1865
|
+
(row) => this.rowToMetricEntry(result[0].columns, row)
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
getMetricCount(options = {}) {
|
|
1869
|
+
this.initializeSync();
|
|
1870
|
+
const conditions = [];
|
|
1871
|
+
const params = [];
|
|
1872
|
+
if (options.name) {
|
|
1873
|
+
conditions.push("name = ?");
|
|
1874
|
+
params.push(options.name);
|
|
1875
|
+
}
|
|
1876
|
+
if (options.type) {
|
|
1877
|
+
conditions.push("type = ?");
|
|
1878
|
+
params.push(options.type);
|
|
1879
|
+
}
|
|
1880
|
+
if (options.service) {
|
|
1881
|
+
conditions.push("service = ?");
|
|
1882
|
+
params.push(options.service);
|
|
1883
|
+
}
|
|
1884
|
+
if (options.tag) {
|
|
1885
|
+
const eqIdx = options.tag.indexOf("=");
|
|
1886
|
+
if (eqIdx > 0) {
|
|
1887
|
+
const tagKey = options.tag.substring(0, eqIdx);
|
|
1888
|
+
const tagValue = options.tag.substring(eqIdx + 1);
|
|
1889
|
+
conditions.push("tags_json LIKE ?");
|
|
1890
|
+
params.push(`%"${tagKey}":"${tagValue}"%`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (options.since) {
|
|
1894
|
+
conditions.push("timestamp >= ?");
|
|
1895
|
+
params.push(options.since);
|
|
1896
|
+
}
|
|
1897
|
+
if (options.until) {
|
|
1898
|
+
conditions.push("timestamp <= ?");
|
|
1899
|
+
params.push(options.until);
|
|
1900
|
+
}
|
|
1901
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1902
|
+
const result = this.db.exec(
|
|
1903
|
+
`SELECT COUNT(*) as count FROM metrics ${whereClause}`,
|
|
1904
|
+
params
|
|
1905
|
+
);
|
|
1906
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
1907
|
+
return result[0].values[0][0];
|
|
1908
|
+
}
|
|
1909
|
+
aggregateMetric(name, options) {
|
|
1910
|
+
this.initializeSync();
|
|
1911
|
+
const conditions = ["name = ?"];
|
|
1912
|
+
const params = [name];
|
|
1913
|
+
if (options?.service) {
|
|
1914
|
+
conditions.push("service = ?");
|
|
1915
|
+
params.push(options.service);
|
|
1916
|
+
}
|
|
1917
|
+
if (options?.since) {
|
|
1918
|
+
conditions.push("timestamp >= ?");
|
|
1919
|
+
params.push(options.since);
|
|
1920
|
+
}
|
|
1921
|
+
if (options?.until) {
|
|
1922
|
+
conditions.push("timestamp <= ?");
|
|
1923
|
+
params.push(options.until);
|
|
1924
|
+
}
|
|
1925
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
1926
|
+
const result = this.db.exec(
|
|
1927
|
+
`SELECT COUNT(*) as count, SUM(value) as sum, MIN(value) as min, MAX(value) as max, AVG(value) as avg
|
|
1928
|
+
FROM metrics ${whereClause}`,
|
|
1929
|
+
params
|
|
1930
|
+
);
|
|
1931
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
1932
|
+
return { name, count: 0, sum: 0, min: 0, max: 0, avg: 0 };
|
|
1933
|
+
}
|
|
1934
|
+
const row = result[0].values[0];
|
|
1935
|
+
return {
|
|
1936
|
+
name,
|
|
1937
|
+
count: row[0] || 0,
|
|
1938
|
+
sum: row[1] || 0,
|
|
1939
|
+
min: row[2] || 0,
|
|
1940
|
+
max: row[3] || 0,
|
|
1941
|
+
avg: row[4] || 0
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
pruneMetrics(maxCount) {
|
|
1945
|
+
this.initializeSync();
|
|
1946
|
+
if (maxCount <= 0) return 0;
|
|
1947
|
+
const currentCount = this.getMetricCount();
|
|
1948
|
+
if (currentCount <= maxCount) return 0;
|
|
1949
|
+
const deleteCount = currentCount - maxCount;
|
|
1950
|
+
this.db.run(
|
|
1951
|
+
`DELETE FROM metrics WHERE id IN (
|
|
1952
|
+
SELECT id FROM metrics ORDER BY timestamp ASC LIMIT ?
|
|
1953
|
+
)`,
|
|
1954
|
+
[deleteCount]
|
|
1955
|
+
);
|
|
1956
|
+
this.save();
|
|
1957
|
+
return deleteCount;
|
|
1958
|
+
}
|
|
1959
|
+
rowToMetricEntry(columns, row) {
|
|
1960
|
+
const obj = {};
|
|
1961
|
+
columns.forEach((col, i) => {
|
|
1962
|
+
obj[col] = row[i];
|
|
1963
|
+
});
|
|
1964
|
+
return {
|
|
1965
|
+
id: obj.id,
|
|
1966
|
+
timestamp: obj.timestamp,
|
|
1967
|
+
name: obj.name,
|
|
1968
|
+
type: obj.type,
|
|
1969
|
+
value: obj.value,
|
|
1970
|
+
tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
|
|
1971
|
+
service: obj.service,
|
|
1972
|
+
environment: obj.environment || void 0
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
// ─── Traces ───────────────────────────────────────────────────
|
|
1976
|
+
insertSpan(input) {
|
|
1977
|
+
this.initializeSync();
|
|
1978
|
+
const spanId = input.spanId || uuidv4();
|
|
1979
|
+
const startTime = input.startTime || (/* @__PURE__ */ new Date()).toISOString();
|
|
1980
|
+
this.db.run(
|
|
1981
|
+
`INSERT INTO traces (
|
|
1982
|
+
trace_id, span_id, parent_span_id, service, symbol, operation,
|
|
1983
|
+
start_time, end_time, duration_ms, status, tags_json, log_ids_json
|
|
1984
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1985
|
+
[
|
|
1986
|
+
input.traceId,
|
|
1987
|
+
spanId,
|
|
1988
|
+
input.parentSpanId || null,
|
|
1989
|
+
input.service,
|
|
1990
|
+
input.symbol,
|
|
1991
|
+
input.operation,
|
|
1992
|
+
startTime,
|
|
1993
|
+
input.endTime || null,
|
|
1994
|
+
input.durationMs ?? null,
|
|
1995
|
+
input.status || "ok",
|
|
1996
|
+
JSON.stringify(input.tags || {}),
|
|
1997
|
+
JSON.stringify(input.logIds || [])
|
|
1998
|
+
]
|
|
1999
|
+
);
|
|
2000
|
+
this.save();
|
|
2001
|
+
return spanId;
|
|
2002
|
+
}
|
|
2003
|
+
getTrace(traceId) {
|
|
2004
|
+
this.initializeSync();
|
|
2005
|
+
const result = this.db.exec(
|
|
2006
|
+
"SELECT * FROM traces WHERE trace_id = ? ORDER BY start_time ASC",
|
|
2007
|
+
[traceId]
|
|
2008
|
+
);
|
|
2009
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
2010
|
+
const spans = result[0].values.map(
|
|
2011
|
+
(row) => this.rowToTraceSpan(result[0].columns, row)
|
|
2012
|
+
);
|
|
2013
|
+
const services = [...new Set(spans.map((s) => s.service))];
|
|
2014
|
+
const startTimes = spans.map((s) => s.startTime);
|
|
2015
|
+
const endTimes = spans.filter((s) => s.endTime).map((s) => s.endTime);
|
|
2016
|
+
const startTime = startTimes.sort()[0];
|
|
2017
|
+
const endTime = endTimes.length > 0 ? endTimes.sort().reverse()[0] : startTime;
|
|
2018
|
+
const startMs = new Date(startTime).getTime();
|
|
2019
|
+
const endMs = new Date(endTime).getTime();
|
|
2020
|
+
const totalDurationMs = endMs - startMs;
|
|
2021
|
+
return {
|
|
2022
|
+
traceId,
|
|
2023
|
+
spans,
|
|
2024
|
+
services,
|
|
2025
|
+
totalDurationMs: totalDurationMs > 0 ? totalDurationMs : 0,
|
|
2026
|
+
startTime,
|
|
2027
|
+
endTime
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
queryTraces(options = {}) {
|
|
2031
|
+
this.initializeSync();
|
|
2032
|
+
const conditions = [];
|
|
2033
|
+
const params = [];
|
|
2034
|
+
if (options.service) {
|
|
2035
|
+
conditions.push("service = ?");
|
|
2036
|
+
params.push(options.service);
|
|
2037
|
+
}
|
|
2038
|
+
if (options.symbol) {
|
|
2039
|
+
conditions.push("symbol = ?");
|
|
2040
|
+
params.push(options.symbol);
|
|
2041
|
+
}
|
|
2042
|
+
if (options.since) {
|
|
2043
|
+
conditions.push("start_time >= ?");
|
|
2044
|
+
params.push(options.since);
|
|
2045
|
+
}
|
|
2046
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2047
|
+
const traceLimit = Math.min(options.limit || 20, 20);
|
|
2048
|
+
const result = this.db.exec(
|
|
2049
|
+
`SELECT DISTINCT trace_id FROM traces ${whereClause} ORDER BY start_time DESC LIMIT ?`,
|
|
2050
|
+
[...params, traceLimit]
|
|
2051
|
+
);
|
|
2052
|
+
if (result.length === 0) return [];
|
|
2053
|
+
const traces = [];
|
|
2054
|
+
for (const row of result[0].values) {
|
|
2055
|
+
const traceId = row[0];
|
|
2056
|
+
const trace = this.getTrace(traceId);
|
|
2057
|
+
if (trace) {
|
|
2058
|
+
traces.push(trace);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return traces;
|
|
2062
|
+
}
|
|
2063
|
+
rowToTraceSpan(columns, row) {
|
|
2064
|
+
const obj = {};
|
|
2065
|
+
columns.forEach((col, i) => {
|
|
2066
|
+
obj[col] = row[i];
|
|
2067
|
+
});
|
|
2068
|
+
return {
|
|
2069
|
+
traceId: obj.trace_id,
|
|
2070
|
+
spanId: obj.span_id,
|
|
2071
|
+
parentSpanId: obj.parent_span_id || void 0,
|
|
2072
|
+
service: obj.service,
|
|
2073
|
+
symbol: obj.symbol,
|
|
2074
|
+
operation: obj.operation,
|
|
2075
|
+
startTime: obj.start_time,
|
|
2076
|
+
endTime: obj.end_time || void 0,
|
|
2077
|
+
durationMs: obj.duration_ms || void 0,
|
|
2078
|
+
status: obj.status || "ok",
|
|
2079
|
+
tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
|
|
2080
|
+
logs: obj.log_ids_json ? JSON.parse(obj.log_ids_json) : []
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
// ─── Schema Registry ─────────────────────────────────────────────
|
|
2084
|
+
registerSchema(schema) {
|
|
2085
|
+
this.initializeSync();
|
|
2086
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2087
|
+
this.db.run(
|
|
2088
|
+
`INSERT INTO schemas (
|
|
2089
|
+
id, version, name, description, scope_json, event_types_json,
|
|
2090
|
+
causality_json, visualization_json, tags_json, registered_at, updated_at
|
|
2091
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2092
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2093
|
+
version = excluded.version,
|
|
2094
|
+
name = excluded.name,
|
|
2095
|
+
description = excluded.description,
|
|
2096
|
+
scope_json = excluded.scope_json,
|
|
2097
|
+
event_types_json = excluded.event_types_json,
|
|
2098
|
+
causality_json = excluded.causality_json,
|
|
2099
|
+
visualization_json = excluded.visualization_json,
|
|
2100
|
+
tags_json = excluded.tags_json,
|
|
2101
|
+
updated_at = excluded.updated_at`,
|
|
2102
|
+
[
|
|
2103
|
+
schema.id,
|
|
2104
|
+
schema.version,
|
|
2105
|
+
schema.name,
|
|
2106
|
+
schema.description || null,
|
|
2107
|
+
JSON.stringify(schema.scope),
|
|
2108
|
+
JSON.stringify(schema.eventTypes),
|
|
2109
|
+
schema.causality ? JSON.stringify(schema.causality) : null,
|
|
2110
|
+
schema.visualization ? JSON.stringify(schema.visualization) : null,
|
|
2111
|
+
JSON.stringify(schema.tags || []),
|
|
2112
|
+
now,
|
|
2113
|
+
now
|
|
2114
|
+
]
|
|
2115
|
+
);
|
|
2116
|
+
this.save();
|
|
2117
|
+
return {
|
|
2118
|
+
id: schema.id,
|
|
2119
|
+
version: schema.version,
|
|
2120
|
+
name: schema.name,
|
|
2121
|
+
description: schema.description,
|
|
2122
|
+
scope: schema.scope,
|
|
2123
|
+
eventTypes: schema.eventTypes,
|
|
2124
|
+
causality: schema.causality,
|
|
2125
|
+
visualization: schema.visualization,
|
|
2126
|
+
tags: schema.tags || [],
|
|
2127
|
+
registeredAt: now,
|
|
2128
|
+
updatedAt: now
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
getSchema(id) {
|
|
2132
|
+
this.initializeSync();
|
|
2133
|
+
const result = this.db.exec("SELECT * FROM schemas WHERE id = ?", [id]);
|
|
2134
|
+
if (result.length === 0 || result[0].values.length === 0) return null;
|
|
2135
|
+
return this.rowToSchema(result[0].columns, result[0].values[0]);
|
|
2136
|
+
}
|
|
2137
|
+
listSchemas() {
|
|
2138
|
+
this.initializeSync();
|
|
2139
|
+
const result = this.db.exec("SELECT * FROM schemas ORDER BY name ASC");
|
|
2140
|
+
if (result.length === 0) return [];
|
|
2141
|
+
return result[0].values.map(
|
|
2142
|
+
(row) => this.rowToSchema(result[0].columns, row)
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
rowToSchema(columns, row) {
|
|
2146
|
+
const obj = {};
|
|
2147
|
+
columns.forEach((col, i) => {
|
|
2148
|
+
obj[col] = row[i];
|
|
2149
|
+
});
|
|
2150
|
+
return {
|
|
2151
|
+
id: obj.id,
|
|
2152
|
+
version: obj.version,
|
|
2153
|
+
name: obj.name,
|
|
2154
|
+
description: obj.description || void 0,
|
|
2155
|
+
scope: JSON.parse(obj.scope_json),
|
|
2156
|
+
eventTypes: JSON.parse(obj.event_types_json),
|
|
2157
|
+
causality: obj.causality_json ? JSON.parse(obj.causality_json) : void 0,
|
|
2158
|
+
visualization: obj.visualization_json ? JSON.parse(obj.visualization_json) : void 0,
|
|
2159
|
+
tags: JSON.parse(obj.tags_json || "[]"),
|
|
2160
|
+
registeredAt: obj.registered_at,
|
|
2161
|
+
updatedAt: obj.updated_at
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
// ─── Generic Events ────────────────────────────────────────────
|
|
2165
|
+
insertEventBatch(schemaId, service, inputs) {
|
|
2166
|
+
this.initializeSync();
|
|
2167
|
+
const schema = this.getSchema(schemaId);
|
|
2168
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
2169
|
+
if (schema) {
|
|
2170
|
+
for (const et of schema.eventTypes) {
|
|
2171
|
+
typeMap.set(et.type, {
|
|
2172
|
+
category: et.category,
|
|
2173
|
+
severity: et.severity || "info"
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
let accepted = 0;
|
|
2178
|
+
const errors = [];
|
|
2179
|
+
for (const input of inputs) {
|
|
2180
|
+
try {
|
|
2181
|
+
const id = input.id || uuidv4();
|
|
2182
|
+
const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
2183
|
+
const resolved = typeMap.get(input.type);
|
|
2184
|
+
const category = resolved?.category || "unknown";
|
|
2185
|
+
const severity = input.severity || resolved?.severity || "info";
|
|
2186
|
+
const scopeValue = input.scopeValue != null ? String(input.scopeValue) : null;
|
|
2187
|
+
const scopeOrdinal = typeof input.scopeValue === "number" ? input.scopeValue : null;
|
|
2188
|
+
this.db.run(
|
|
2189
|
+
`INSERT INTO events (
|
|
2190
|
+
id, schema_id, event_type, category, timestamp, scope_value,
|
|
2191
|
+
scope_ordinal, session_id, service, data_json, severity,
|
|
2192
|
+
parent_event_id, depth
|
|
2193
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2194
|
+
[
|
|
2195
|
+
id,
|
|
2196
|
+
schemaId,
|
|
2197
|
+
input.type,
|
|
2198
|
+
category,
|
|
2199
|
+
timestamp,
|
|
2200
|
+
scopeValue,
|
|
2201
|
+
scopeOrdinal,
|
|
2202
|
+
input.sessionId || null,
|
|
2203
|
+
service,
|
|
2204
|
+
input.data ? JSON.stringify(input.data) : null,
|
|
2205
|
+
severity,
|
|
2206
|
+
input.parentEventId || null,
|
|
2207
|
+
input.depth ?? 0
|
|
2208
|
+
]
|
|
2209
|
+
);
|
|
2210
|
+
accepted++;
|
|
2211
|
+
} catch (err) {
|
|
2212
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
this.save();
|
|
2216
|
+
return { accepted, errors };
|
|
2217
|
+
}
|
|
2218
|
+
queryEvents(options = {}) {
|
|
2219
|
+
this.initializeSync();
|
|
2220
|
+
const { limit = 100, offset = 0 } = options;
|
|
2221
|
+
const conditions = [];
|
|
2222
|
+
const params = [];
|
|
2223
|
+
if (options.schemaId) {
|
|
2224
|
+
conditions.push("schema_id = ?");
|
|
2225
|
+
params.push(options.schemaId);
|
|
2226
|
+
}
|
|
2227
|
+
if (options.eventType) {
|
|
2228
|
+
conditions.push("event_type = ?");
|
|
2229
|
+
params.push(options.eventType);
|
|
2230
|
+
}
|
|
2231
|
+
if (options.category) {
|
|
2232
|
+
conditions.push("category = ?");
|
|
2233
|
+
params.push(options.category);
|
|
2234
|
+
}
|
|
2235
|
+
if (options.service) {
|
|
2236
|
+
conditions.push("service = ?");
|
|
2237
|
+
params.push(options.service);
|
|
2238
|
+
}
|
|
2239
|
+
if (options.sessionId) {
|
|
2240
|
+
conditions.push("session_id = ?");
|
|
2241
|
+
params.push(options.sessionId);
|
|
2242
|
+
}
|
|
2243
|
+
if (options.scopeValue) {
|
|
2244
|
+
conditions.push("scope_value = ?");
|
|
2245
|
+
params.push(options.scopeValue);
|
|
2246
|
+
}
|
|
2247
|
+
if (options.scopeFrom) {
|
|
2248
|
+
conditions.push("scope_value >= ?");
|
|
2249
|
+
params.push(options.scopeFrom);
|
|
2250
|
+
}
|
|
2251
|
+
if (options.scopeTo) {
|
|
2252
|
+
conditions.push("scope_value <= ?");
|
|
2253
|
+
params.push(options.scopeTo);
|
|
2254
|
+
}
|
|
2255
|
+
if (options.severity) {
|
|
2256
|
+
conditions.push("severity = ?");
|
|
2257
|
+
params.push(options.severity);
|
|
2258
|
+
}
|
|
2259
|
+
if (options.since) {
|
|
2260
|
+
conditions.push("timestamp >= ?");
|
|
2261
|
+
params.push(options.since);
|
|
2262
|
+
}
|
|
2263
|
+
if (options.until) {
|
|
2264
|
+
conditions.push("timestamp <= ?");
|
|
2265
|
+
params.push(options.until);
|
|
2266
|
+
}
|
|
2267
|
+
if (options.search) {
|
|
2268
|
+
conditions.push("data_json LIKE ?");
|
|
2269
|
+
params.push(`%${options.search}%`);
|
|
2270
|
+
}
|
|
2271
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2272
|
+
const result = this.db.exec(
|
|
2273
|
+
`SELECT * FROM events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
|
|
2274
|
+
[...params, limit, offset]
|
|
2275
|
+
);
|
|
2276
|
+
if (result.length === 0) return [];
|
|
2277
|
+
return result[0].values.map(
|
|
2278
|
+
(row) => this.rowToGenericEvent(result[0].columns, row)
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
queryEventsByScope(schemaId, scopeValue) {
|
|
2282
|
+
this.initializeSync();
|
|
2283
|
+
const result = this.db.exec(
|
|
2284
|
+
`SELECT * FROM events
|
|
2285
|
+
WHERE schema_id = ? AND scope_value = ?
|
|
2286
|
+
ORDER BY timestamp ASC`,
|
|
2287
|
+
[schemaId, scopeValue]
|
|
2288
|
+
);
|
|
2289
|
+
if (result.length === 0) return [];
|
|
2290
|
+
return result[0].values.map(
|
|
2291
|
+
(row) => this.rowToGenericEvent(result[0].columns, row)
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
getEventScopes(schemaId, options = {}) {
|
|
2295
|
+
this.initializeSync();
|
|
2296
|
+
const { limit = 100, offset = 0 } = options;
|
|
2297
|
+
const conditions = ["schema_id = ?"];
|
|
2298
|
+
const params = [schemaId];
|
|
2299
|
+
if (options.sessionId) {
|
|
2300
|
+
conditions.push("session_id = ?");
|
|
2301
|
+
params.push(options.sessionId);
|
|
2302
|
+
}
|
|
2303
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
2304
|
+
const result = this.db.exec(
|
|
2305
|
+
`SELECT
|
|
2306
|
+
scope_value,
|
|
2307
|
+
MIN(scope_ordinal) as scope_ordinal,
|
|
2308
|
+
COUNT(*) as event_count,
|
|
2309
|
+
MIN(timestamp) as first_timestamp,
|
|
2310
|
+
MAX(timestamp) as last_timestamp
|
|
2311
|
+
FROM events
|
|
2312
|
+
${whereClause}
|
|
2313
|
+
AND scope_value IS NOT NULL
|
|
2314
|
+
GROUP BY scope_value
|
|
2315
|
+
ORDER BY MIN(COALESCE(scope_ordinal, 0)) DESC, MIN(timestamp) DESC
|
|
2316
|
+
LIMIT ? OFFSET ?`,
|
|
2317
|
+
[...params, limit, offset]
|
|
2318
|
+
);
|
|
2319
|
+
if (result.length === 0) return [];
|
|
2320
|
+
const scopes = [];
|
|
2321
|
+
for (const row of result[0].values) {
|
|
2322
|
+
const scopeValue = row[0];
|
|
2323
|
+
const scopeOrdinal = row[1] != null ? row[1] : void 0;
|
|
2324
|
+
const eventCount = row[2];
|
|
2325
|
+
const firstTimestamp = row[3];
|
|
2326
|
+
const lastTimestamp = row[4];
|
|
2327
|
+
const catResult = this.db.exec(
|
|
2328
|
+
`SELECT category, COUNT(*) as count FROM events
|
|
2329
|
+
WHERE schema_id = ? AND scope_value = ?
|
|
2330
|
+
GROUP BY category`,
|
|
2331
|
+
[schemaId, scopeValue]
|
|
2332
|
+
);
|
|
2333
|
+
const categories = {};
|
|
2334
|
+
if (catResult.length > 0) {
|
|
2335
|
+
for (const catRow of catResult[0].values) {
|
|
2336
|
+
categories[catRow[0]] = catRow[1];
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
scopes.push({
|
|
2340
|
+
scopeValue,
|
|
2341
|
+
scopeOrdinal,
|
|
2342
|
+
eventCount,
|
|
2343
|
+
categories,
|
|
2344
|
+
firstTimestamp,
|
|
2345
|
+
lastTimestamp
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
return scopes;
|
|
2349
|
+
}
|
|
2350
|
+
getEventCount(options = {}) {
|
|
2351
|
+
this.initializeSync();
|
|
2352
|
+
const conditions = [];
|
|
2353
|
+
const params = [];
|
|
2354
|
+
if (options.schemaId) {
|
|
2355
|
+
conditions.push("schema_id = ?");
|
|
2356
|
+
params.push(options.schemaId);
|
|
2357
|
+
}
|
|
2358
|
+
if (options.eventType) {
|
|
2359
|
+
conditions.push("event_type = ?");
|
|
2360
|
+
params.push(options.eventType);
|
|
2361
|
+
}
|
|
2362
|
+
if (options.service) {
|
|
2363
|
+
conditions.push("service = ?");
|
|
2364
|
+
params.push(options.service);
|
|
2365
|
+
}
|
|
2366
|
+
if (options.since) {
|
|
2367
|
+
conditions.push("timestamp >= ?");
|
|
2368
|
+
params.push(options.since);
|
|
2369
|
+
}
|
|
2370
|
+
if (options.until) {
|
|
2371
|
+
conditions.push("timestamp <= ?");
|
|
2372
|
+
params.push(options.until);
|
|
2373
|
+
}
|
|
2374
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2375
|
+
const result = this.db.exec(
|
|
2376
|
+
`SELECT COUNT(*) as count FROM events ${whereClause}`,
|
|
2377
|
+
params
|
|
2378
|
+
);
|
|
2379
|
+
if (result.length === 0 || result[0].values.length === 0) return 0;
|
|
2380
|
+
return result[0].values[0][0];
|
|
2381
|
+
}
|
|
2382
|
+
pruneEvents(maxCount) {
|
|
2383
|
+
this.initializeSync();
|
|
2384
|
+
if (maxCount <= 0) return 0;
|
|
2385
|
+
const currentCount = this.getEventCount();
|
|
2386
|
+
if (currentCount <= maxCount) return 0;
|
|
2387
|
+
const deleteCount = currentCount - maxCount;
|
|
2388
|
+
this.db.run(
|
|
2389
|
+
`DELETE FROM events WHERE id IN (
|
|
2390
|
+
SELECT id FROM events ORDER BY timestamp ASC LIMIT ?
|
|
2391
|
+
)`,
|
|
2392
|
+
[deleteCount]
|
|
2393
|
+
);
|
|
2394
|
+
this.save();
|
|
2395
|
+
return deleteCount;
|
|
2396
|
+
}
|
|
2397
|
+
rowToGenericEvent(columns, row) {
|
|
2398
|
+
const obj = {};
|
|
2399
|
+
columns.forEach((col, i) => {
|
|
2400
|
+
obj[col] = row[i];
|
|
2401
|
+
});
|
|
2402
|
+
return {
|
|
2403
|
+
id: obj.id,
|
|
2404
|
+
schemaId: obj.schema_id,
|
|
2405
|
+
eventType: obj.event_type,
|
|
2406
|
+
category: obj.category,
|
|
2407
|
+
timestamp: obj.timestamp,
|
|
2408
|
+
scopeValue: obj.scope_value || void 0,
|
|
2409
|
+
scopeOrdinal: obj.scope_ordinal != null ? obj.scope_ordinal : void 0,
|
|
2410
|
+
sessionId: obj.session_id || void 0,
|
|
2411
|
+
service: obj.service,
|
|
2412
|
+
data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
|
|
2413
|
+
severity: obj.severity || "info",
|
|
2414
|
+
parentEventId: obj.parent_event_id || void 0,
|
|
2415
|
+
depth: obj.depth || 0
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
close() {
|
|
2419
|
+
if (this.db) {
|
|
2420
|
+
this.save();
|
|
2421
|
+
this.db.close();
|
|
2422
|
+
this.db = null;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
|
|
2427
|
+
// ../sentinel/src/matcher.ts
|
|
2428
|
+
var DEFAULT_CONFIG = {
|
|
2429
|
+
minScore: 30,
|
|
2430
|
+
maxResults: 5,
|
|
2431
|
+
boostConfidence: true
|
|
2432
|
+
};
|
|
2433
|
+
var PatternMatcher = class {
|
|
2434
|
+
constructor(storage2) {
|
|
2435
|
+
this.storage = storage2;
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Match an incident against all patterns and return ranked results
|
|
2439
|
+
*/
|
|
2440
|
+
match(incident, config = {}) {
|
|
2441
|
+
const { minScore, maxResults, boostConfidence } = {
|
|
2442
|
+
...DEFAULT_CONFIG,
|
|
2443
|
+
...config
|
|
2444
|
+
};
|
|
2445
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
2446
|
+
const matches = [];
|
|
2447
|
+
for (const pattern of patterns) {
|
|
2448
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const { score, matchedCriteria } = this.scoreMatch(pattern, incident);
|
|
2452
|
+
if (score >= minScore) {
|
|
2453
|
+
let confidence = score;
|
|
2454
|
+
if (boostConfidence) {
|
|
2455
|
+
const confidenceFactor = pattern.confidence.score / 100;
|
|
2456
|
+
confidence = score * (0.5 + 0.5 * confidenceFactor);
|
|
2457
|
+
}
|
|
2458
|
+
matches.push({
|
|
2459
|
+
pattern,
|
|
2460
|
+
score,
|
|
2461
|
+
matchedCriteria,
|
|
2462
|
+
confidence: Math.round(confidence)
|
|
2463
|
+
});
|
|
2464
|
+
this.storage.updatePatternConfidence(pattern.id, "matched");
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return matches.sort((a, b) => b.confidence - a.confidence).slice(0, maxResults);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Test a pattern against historical incidents
|
|
2471
|
+
*/
|
|
2472
|
+
testPattern(pattern, limit = 100) {
|
|
2473
|
+
const incidents = this.storage.getRecentIncidents({ limit });
|
|
2474
|
+
const wouldMatch = [];
|
|
2475
|
+
let totalScore = 0;
|
|
2476
|
+
for (const incident of incidents) {
|
|
2477
|
+
if (!this.matchEnvironment(pattern, incident)) {
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
const { score } = this.scoreMatch(pattern, incident);
|
|
2481
|
+
if (score >= 30) {
|
|
2482
|
+
wouldMatch.push(incident);
|
|
2483
|
+
totalScore += score;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return {
|
|
2487
|
+
wouldMatch,
|
|
2488
|
+
matchCount: wouldMatch.length,
|
|
2489
|
+
avgScore: wouldMatch.length > 0 ? Math.round(totalScore / wouldMatch.length) : 0
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Score how well a pattern matches an incident
|
|
2494
|
+
*/
|
|
2495
|
+
scoreMatch(pattern, incident) {
|
|
2496
|
+
let score = 0;
|
|
2497
|
+
const matchedCriteria = {
|
|
2498
|
+
symbols: [],
|
|
2499
|
+
errorKeywords: [],
|
|
2500
|
+
missingSignals: []
|
|
2501
|
+
};
|
|
2502
|
+
const symbolScore = this.matchSymbols(
|
|
2503
|
+
pattern.pattern.symbols,
|
|
2504
|
+
incident.symbols,
|
|
2505
|
+
matchedCriteria.symbols
|
|
2506
|
+
);
|
|
2507
|
+
score += Math.min(symbolScore, 50);
|
|
2508
|
+
const errorScore = this.matchErrorText(
|
|
2509
|
+
pattern,
|
|
2510
|
+
incident,
|
|
2511
|
+
matchedCriteria.errorKeywords
|
|
2512
|
+
);
|
|
2513
|
+
score += Math.min(errorScore, 25);
|
|
2514
|
+
const signalScore = this.matchMissingSignals(
|
|
2515
|
+
pattern,
|
|
2516
|
+
incident,
|
|
2517
|
+
matchedCriteria.missingSignals
|
|
2518
|
+
);
|
|
2519
|
+
score += Math.min(signalScore, 25);
|
|
2520
|
+
score = Math.min(score, 100);
|
|
2521
|
+
return { score, matchedCriteria };
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Match symbols between pattern and incident
|
|
2525
|
+
*/
|
|
2526
|
+
matchSymbols(patternSymbols, incidentSymbols, matched) {
|
|
2527
|
+
let score = 0;
|
|
2528
|
+
const symbolTypes = [
|
|
2529
|
+
"feature",
|
|
2530
|
+
"component",
|
|
2531
|
+
"flow",
|
|
2532
|
+
"gate",
|
|
2533
|
+
"signal",
|
|
2534
|
+
"state",
|
|
2535
|
+
"integration"
|
|
2536
|
+
];
|
|
2537
|
+
for (const type of symbolTypes) {
|
|
2538
|
+
const patternValue = patternSymbols[type];
|
|
2539
|
+
const incidentValue = incidentSymbols[type];
|
|
2540
|
+
if (!patternValue || !incidentValue) {
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
if (typeof patternValue === "string") {
|
|
2544
|
+
if (this.matchSingleSymbol(patternValue, incidentValue)) {
|
|
2545
|
+
score += patternValue.includes("*") ? 5 : 10;
|
|
2546
|
+
matched.push(type);
|
|
2547
|
+
}
|
|
2548
|
+
} else if (Array.isArray(patternValue)) {
|
|
2549
|
+
for (const pv of patternValue) {
|
|
2550
|
+
if (this.matchSingleSymbol(pv, incidentValue)) {
|
|
2551
|
+
score += 7;
|
|
2552
|
+
matched.push(type);
|
|
2553
|
+
break;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return score;
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Match a single symbol value (supports wildcards)
|
|
2562
|
+
*/
|
|
2563
|
+
matchSingleSymbol(pattern, value) {
|
|
2564
|
+
if (pattern === "*") {
|
|
2565
|
+
return true;
|
|
2566
|
+
}
|
|
2567
|
+
if (pattern.endsWith("*")) {
|
|
2568
|
+
const prefix = pattern.slice(0, -1);
|
|
2569
|
+
return value.startsWith(prefix);
|
|
2570
|
+
}
|
|
2571
|
+
if (pattern.startsWith("*")) {
|
|
2572
|
+
const suffix = pattern.slice(1);
|
|
2573
|
+
return value.endsWith(suffix);
|
|
2574
|
+
}
|
|
2575
|
+
if (pattern.includes("*")) {
|
|
2576
|
+
const regex = new RegExp(
|
|
2577
|
+
"^" + pattern.replace(/\*/g, ".*") + "$"
|
|
2578
|
+
);
|
|
2579
|
+
return regex.test(value);
|
|
2580
|
+
}
|
|
2581
|
+
return pattern === value;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Match error text keywords and regex
|
|
2585
|
+
*/
|
|
2586
|
+
matchErrorText(pattern, incident, matched) {
|
|
2587
|
+
let score = 0;
|
|
2588
|
+
const errorMessage = incident.error.message.toLowerCase();
|
|
2589
|
+
const errorType = incident.error.type?.toLowerCase();
|
|
2590
|
+
if (pattern.pattern.errorContains) {
|
|
2591
|
+
for (const keyword of pattern.pattern.errorContains) {
|
|
2592
|
+
if (errorMessage.includes(keyword.toLowerCase())) {
|
|
2593
|
+
score += 5;
|
|
2594
|
+
matched.push(keyword);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
if (pattern.pattern.errorMatches) {
|
|
2599
|
+
try {
|
|
2600
|
+
const regex = new RegExp(pattern.pattern.errorMatches, "i");
|
|
2601
|
+
if (regex.test(incident.error.message)) {
|
|
2602
|
+
score += 10;
|
|
2603
|
+
matched.push(`regex:${pattern.pattern.errorMatches}`);
|
|
2604
|
+
}
|
|
2605
|
+
} catch {
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
if (pattern.pattern.errorType && errorType) {
|
|
2609
|
+
for (const type of pattern.pattern.errorType) {
|
|
2610
|
+
if (errorType.includes(type.toLowerCase())) {
|
|
2611
|
+
score += 5;
|
|
2612
|
+
matched.push(`type:${type}`);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return score;
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Match missing signals from flow position
|
|
2620
|
+
*/
|
|
2621
|
+
matchMissingSignals(pattern, incident, matched) {
|
|
2622
|
+
if (!pattern.pattern.missingSignals || !incident.flowPosition?.missing) {
|
|
2623
|
+
return 0;
|
|
2624
|
+
}
|
|
2625
|
+
let score = 0;
|
|
2626
|
+
for (const expectedSignal of pattern.pattern.missingSignals) {
|
|
2627
|
+
for (const missingSignal of incident.flowPosition.missing) {
|
|
2628
|
+
if (this.matchSingleSymbol(expectedSignal, missingSignal)) {
|
|
2629
|
+
score += 12;
|
|
2630
|
+
matched.push(missingSignal);
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
return score;
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Check if pattern's environment filter matches incident
|
|
2639
|
+
*/
|
|
2640
|
+
matchEnvironment(pattern, incident) {
|
|
2641
|
+
if (!pattern.pattern.environment || pattern.pattern.environment.length === 0) {
|
|
2642
|
+
return true;
|
|
2643
|
+
}
|
|
2644
|
+
return pattern.pattern.environment.includes(incident.environment);
|
|
2645
|
+
}
|
|
2646
|
+
};
|
|
2647
|
+
|
|
2648
|
+
// ../sentinel/src/timeline.ts
|
|
2649
|
+
var TimelineBuilder = class {
|
|
2650
|
+
/**
|
|
2651
|
+
* Build a timeline from an incident with flow position
|
|
2652
|
+
*/
|
|
2653
|
+
build(incident) {
|
|
2654
|
+
if (!incident.flowPosition) {
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
const events = [];
|
|
2658
|
+
const baseTime = new Date(incident.timestamp).getTime();
|
|
2659
|
+
events.push({
|
|
2660
|
+
timestamp: new Date(baseTime - 5e3).toISOString(),
|
|
2661
|
+
symbol: incident.flowPosition.flowId,
|
|
2662
|
+
type: "flow-started"
|
|
2663
|
+
});
|
|
2664
|
+
let eventOffset = 1e3;
|
|
2665
|
+
for (const signal of incident.flowPosition.actual) {
|
|
2666
|
+
const type = this.inferEventType(signal);
|
|
2667
|
+
events.push({
|
|
2668
|
+
timestamp: new Date(baseTime - 4e3 + eventOffset).toISOString(),
|
|
2669
|
+
symbol: signal,
|
|
2670
|
+
type
|
|
2671
|
+
});
|
|
2672
|
+
eventOffset += Math.random() * 1e3 + 500;
|
|
2673
|
+
}
|
|
2674
|
+
const failedSymbol = incident.flowPosition.failedAt || incident.flowPosition.missing[0] || incident.symbols.gate || incident.symbols.signal || "unknown";
|
|
2675
|
+
events.push({
|
|
2676
|
+
timestamp: incident.timestamp,
|
|
2677
|
+
symbol: failedSymbol,
|
|
2678
|
+
type: "error",
|
|
2679
|
+
data: {
|
|
2680
|
+
message: incident.error.message,
|
|
2681
|
+
missing: incident.flowPosition.missing
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
return {
|
|
2685
|
+
incidentId: incident.id,
|
|
2686
|
+
flowId: incident.flowPosition.flowId,
|
|
2687
|
+
events,
|
|
2688
|
+
failure: {
|
|
2689
|
+
at: incident.timestamp,
|
|
2690
|
+
symbol: failedSymbol,
|
|
2691
|
+
reason: incident.error.message
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Render timeline as ASCII art
|
|
2697
|
+
*/
|
|
2698
|
+
renderAscii(timeline) {
|
|
2699
|
+
const lines = [];
|
|
2700
|
+
lines.push(`${timeline.flowId} Timeline`);
|
|
2701
|
+
lines.push("\u2550".repeat(40));
|
|
2702
|
+
lines.push("");
|
|
2703
|
+
for (const event of timeline.events) {
|
|
2704
|
+
const time = this.formatTime(event.timestamp);
|
|
2705
|
+
const icon = this.getEventIcon(event.type);
|
|
2706
|
+
const status = this.getEventStatus(event.type);
|
|
2707
|
+
let line = `${time} ${icon} ${event.symbol}`;
|
|
2708
|
+
if (status) {
|
|
2709
|
+
line += ` (${status})`;
|
|
2710
|
+
}
|
|
2711
|
+
lines.push(line);
|
|
2712
|
+
if (event.type === "error" && event.data) {
|
|
2713
|
+
lines.push(` \u2514\u2500 ${event.data.message}`);
|
|
2714
|
+
if (event.data.missing && Array.isArray(event.data.missing) && event.data.missing.length > 0) {
|
|
2715
|
+
lines.push(
|
|
2716
|
+
` \u2514\u2500 Expected: ${event.data.missing.join(", ")}`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
const missing = timeline.events.find((e) => e.type === "error")?.data?.missing;
|
|
2722
|
+
if (missing && missing.length > 0) {
|
|
2723
|
+
lines.push("");
|
|
2724
|
+
lines.push(`Missing signals: ${missing.join(", ")}`);
|
|
2725
|
+
}
|
|
2726
|
+
return lines.join("\n");
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Render timeline as structured data (for MCP/JSON output)
|
|
2730
|
+
*/
|
|
2731
|
+
renderStructured(timeline) {
|
|
2732
|
+
return {
|
|
2733
|
+
incidentId: timeline.incidentId,
|
|
2734
|
+
flow: {
|
|
2735
|
+
id: timeline.flowId,
|
|
2736
|
+
eventCount: timeline.events.length
|
|
2737
|
+
},
|
|
2738
|
+
events: timeline.events.map((event) => ({
|
|
2739
|
+
time: this.formatTime(event.timestamp),
|
|
2740
|
+
symbol: event.symbol,
|
|
2741
|
+
type: event.type,
|
|
2742
|
+
status: this.getEventStatus(event.type),
|
|
2743
|
+
data: event.data
|
|
2744
|
+
})),
|
|
2745
|
+
failure: {
|
|
2746
|
+
at: this.formatTime(timeline.failure.at),
|
|
2747
|
+
symbol: timeline.failure.symbol,
|
|
2748
|
+
reason: timeline.failure.reason
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Infer event type from symbol prefix
|
|
2754
|
+
*/
|
|
2755
|
+
inferEventType(symbol) {
|
|
2756
|
+
if (symbol.startsWith("^")) {
|
|
2757
|
+
return "gate-passed";
|
|
2758
|
+
}
|
|
2759
|
+
if (symbol.startsWith("!")) {
|
|
2760
|
+
return "signal-emitted";
|
|
2761
|
+
}
|
|
2762
|
+
if (symbol.startsWith("%")) {
|
|
2763
|
+
return "state-changed";
|
|
2764
|
+
}
|
|
2765
|
+
return "signal-emitted";
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Get icon for event type
|
|
2769
|
+
*/
|
|
2770
|
+
getEventIcon(type) {
|
|
2771
|
+
switch (type) {
|
|
2772
|
+
case "flow-started":
|
|
2773
|
+
return "\u25B6";
|
|
2774
|
+
case "flow-ended":
|
|
2775
|
+
return "\u25A0";
|
|
2776
|
+
case "gate-passed":
|
|
2777
|
+
return "\u2713";
|
|
2778
|
+
case "gate-failed":
|
|
2779
|
+
return "\u2717";
|
|
2780
|
+
case "signal-emitted":
|
|
2781
|
+
return "\u26A1";
|
|
2782
|
+
case "state-changed":
|
|
2783
|
+
return "\u25C6";
|
|
2784
|
+
case "error":
|
|
2785
|
+
return "\u2717";
|
|
2786
|
+
default:
|
|
2787
|
+
return "\u2022";
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Get status text for event type
|
|
2792
|
+
*/
|
|
2793
|
+
getEventStatus(type) {
|
|
2794
|
+
switch (type) {
|
|
2795
|
+
case "gate-passed":
|
|
2796
|
+
return "PASSED";
|
|
2797
|
+
case "gate-failed":
|
|
2798
|
+
return "FAILED";
|
|
2799
|
+
case "signal-emitted":
|
|
2800
|
+
return "EMITTED";
|
|
2801
|
+
case "state-changed":
|
|
2802
|
+
return "CHANGED";
|
|
2803
|
+
case "error":
|
|
2804
|
+
return "ERROR";
|
|
2805
|
+
default:
|
|
2806
|
+
return "";
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Format timestamp for display
|
|
2811
|
+
*/
|
|
2812
|
+
formatTime(timestamp) {
|
|
2813
|
+
const date = new Date(timestamp);
|
|
2814
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
2815
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
2816
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
2817
|
+
const millis = String(date.getMilliseconds()).padStart(3, "0");
|
|
2818
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
|
|
2822
|
+
// ../sentinel/src/stats.ts
|
|
2823
|
+
var StatsCalculator = class {
|
|
2824
|
+
constructor(storage2) {
|
|
2825
|
+
this.storage = storage2;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Get comprehensive statistics for a time period
|
|
2829
|
+
*/
|
|
2830
|
+
getStats(periodDays = 7) {
|
|
2831
|
+
const end = (/* @__PURE__ */ new Date()).toISOString();
|
|
2832
|
+
const start = new Date(
|
|
2833
|
+
Date.now() - periodDays * 24 * 60 * 60 * 1e3
|
|
2834
|
+
).toISOString();
|
|
2835
|
+
return this.storage.getStats({ start, end });
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Get health metrics for a specific symbol
|
|
2839
|
+
*/
|
|
2840
|
+
getSymbolHealth(symbol) {
|
|
2841
|
+
return this.storage.getSymbolHealth(symbol);
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Get trending issues (symbols with increasing incident rates)
|
|
2845
|
+
*/
|
|
2846
|
+
getTrendingIssues(days = 7) {
|
|
2847
|
+
const now = Date.now();
|
|
2848
|
+
const halfPeriod = days * 24 * 60 * 60 * 1e3 / 2;
|
|
2849
|
+
const firstHalfStart = new Date(now - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
2850
|
+
const midpoint = new Date(now - halfPeriod).toISOString();
|
|
2851
|
+
const secondHalfEnd = new Date(now).toISOString();
|
|
2852
|
+
const firstHalfIncidents = this.storage.getRecentIncidents({
|
|
2853
|
+
dateFrom: firstHalfStart,
|
|
2854
|
+
dateTo: midpoint,
|
|
2855
|
+
limit: 1e3
|
|
2856
|
+
});
|
|
2857
|
+
const secondHalfIncidents = this.storage.getRecentIncidents({
|
|
2858
|
+
dateFrom: midpoint,
|
|
2859
|
+
dateTo: secondHalfEnd,
|
|
2860
|
+
limit: 1e3
|
|
2861
|
+
});
|
|
2862
|
+
const firstHalfCounts = this.countSymbols(firstHalfIncidents);
|
|
2863
|
+
const secondHalfCounts = this.countSymbols(secondHalfIncidents);
|
|
2864
|
+
const trends = [];
|
|
2865
|
+
const allSymbols = /* @__PURE__ */ new Set([
|
|
2866
|
+
...firstHalfCounts.keys(),
|
|
2867
|
+
...secondHalfCounts.keys()
|
|
2868
|
+
]);
|
|
2869
|
+
for (const symbol of allSymbols) {
|
|
2870
|
+
const first = firstHalfCounts.get(symbol) || 0;
|
|
2871
|
+
const second = secondHalfCounts.get(symbol) || 0;
|
|
2872
|
+
if (first === 0 && second > 0) {
|
|
2873
|
+
trends.push({ symbol, trend: second * 100 });
|
|
2874
|
+
} else if (first > 0) {
|
|
2875
|
+
const change = (second - first) / first * 100;
|
|
2876
|
+
trends.push({ symbol, trend: change });
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
return trends.filter((t) => t.trend > 0).sort((a, b) => b.trend - a.trend).slice(0, 10);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Get resolution metrics
|
|
2883
|
+
*/
|
|
2884
|
+
getResolutionMetrics() {
|
|
2885
|
+
const stats = this.getStats(30);
|
|
2886
|
+
return {
|
|
2887
|
+
avgTimeToResolve: stats.resolution.avgTimeToResolve,
|
|
2888
|
+
resolvedWithPattern: stats.resolution.resolvedWithPattern,
|
|
2889
|
+
resolvedManually: stats.resolution.resolvedManually,
|
|
2890
|
+
totalResolved: stats.incidents.resolved,
|
|
2891
|
+
resolutionRate: stats.resolution.resolutionRate
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Get pattern effectiveness metrics
|
|
2896
|
+
*/
|
|
2897
|
+
getPatternEffectiveness() {
|
|
2898
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
2899
|
+
return patterns.filter((p) => p.confidence.timesMatched > 0).map((p) => ({
|
|
2900
|
+
patternId: p.id,
|
|
2901
|
+
name: p.name,
|
|
2902
|
+
matches: p.confidence.timesMatched,
|
|
2903
|
+
resolutions: p.confidence.timesResolved,
|
|
2904
|
+
recurrences: p.confidence.timesRecurred,
|
|
2905
|
+
effectiveness: p.confidence.timesMatched > 0 ? Math.round(
|
|
2906
|
+
(p.confidence.timesResolved - p.confidence.timesRecurred) / p.confidence.timesMatched * 100
|
|
2907
|
+
) : 0
|
|
2908
|
+
})).sort((a, b) => b.effectiveness - a.effectiveness);
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Get incident rate by hour of day
|
|
2912
|
+
*/
|
|
2913
|
+
getIncidentsByHour(days = 7) {
|
|
2914
|
+
const start = new Date(
|
|
2915
|
+
Date.now() - days * 24 * 60 * 60 * 1e3
|
|
2916
|
+
).toISOString();
|
|
2917
|
+
const incidents = this.storage.getRecentIncidents({
|
|
2918
|
+
dateFrom: start,
|
|
2919
|
+
limit: 1e4
|
|
2920
|
+
});
|
|
2921
|
+
const hourCounts = /* @__PURE__ */ new Map();
|
|
2922
|
+
for (let i = 0; i < 24; i++) {
|
|
2923
|
+
hourCounts.set(i, 0);
|
|
2924
|
+
}
|
|
2925
|
+
for (const incident of incidents) {
|
|
2926
|
+
const hour = new Date(incident.timestamp).getHours();
|
|
2927
|
+
hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
|
|
2928
|
+
}
|
|
2929
|
+
return Array.from(hourCounts.entries()).map(([hour, count]) => ({
|
|
2930
|
+
hour,
|
|
2931
|
+
count
|
|
2932
|
+
}));
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Get incident rate by environment
|
|
2936
|
+
*/
|
|
2937
|
+
getIncidentsByEnvironment() {
|
|
2938
|
+
const stats = this.getStats(30);
|
|
2939
|
+
const total = stats.incidents.total;
|
|
2940
|
+
return Object.entries(stats.incidents.byEnvironment).map(([environment, count]) => ({
|
|
2941
|
+
environment,
|
|
2942
|
+
count,
|
|
2943
|
+
percentage: total > 0 ? Math.round(count / total * 100) : 0
|
|
2944
|
+
})).sort((a, b) => b.count - a.count);
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Get symbol correlation matrix (which symbols fail together)
|
|
2948
|
+
*/
|
|
2949
|
+
getSymbolCorrelation() {
|
|
2950
|
+
const incidents = this.storage.getRecentIncidents({ limit: 1e3 });
|
|
2951
|
+
const correlations = /* @__PURE__ */ new Map();
|
|
2952
|
+
const symbolCounts = /* @__PURE__ */ new Map();
|
|
2953
|
+
for (const incident of incidents) {
|
|
2954
|
+
const symbols = this.getSymbolsFromIncident(incident);
|
|
2955
|
+
for (const symbol of symbols) {
|
|
2956
|
+
symbolCounts.set(symbol, (symbolCounts.get(symbol) || 0) + 1);
|
|
2957
|
+
}
|
|
2958
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
2959
|
+
for (let j = i + 1; j < symbols.length; j++) {
|
|
2960
|
+
const key = [symbols[i], symbols[j]].sort().join("|");
|
|
2961
|
+
correlations.set(key, (correlations.get(key) || 0) + 1);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
const results = [];
|
|
2966
|
+
for (const [key, count] of correlations) {
|
|
2967
|
+
const [symbol1, symbol2] = key.split("|");
|
|
2968
|
+
const count1 = symbolCounts.get(symbol1) || 1;
|
|
2969
|
+
const count2 = symbolCounts.get(symbol2) || 1;
|
|
2970
|
+
const correlation = count / Math.max(count1, count2);
|
|
2971
|
+
if (correlation > 0.3) {
|
|
2972
|
+
results.push({
|
|
2973
|
+
symbol1,
|
|
2974
|
+
symbol2,
|
|
2975
|
+
correlation: Math.round(correlation * 100) / 100
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
return results.sort((a, b) => b.correlation - a.correlation).slice(0, 20);
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Generate a summary dashboard string
|
|
2983
|
+
*/
|
|
2984
|
+
generateDashboard(periodDays = 7) {
|
|
2985
|
+
const stats = this.getStats(periodDays);
|
|
2986
|
+
const lines = [];
|
|
2987
|
+
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");
|
|
2988
|
+
lines.push("\u2551 PARADIGM SENTINEL DASHBOARD \u2551");
|
|
2989
|
+
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");
|
|
2990
|
+
const todayCount = stats.incidents.byDay[stats.incidents.byDay.length - 1]?.count || 0;
|
|
2991
|
+
lines.push(
|
|
2992
|
+
`\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`
|
|
2993
|
+
);
|
|
2994
|
+
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");
|
|
2995
|
+
lines.push("");
|
|
2996
|
+
lines.push("Incidents by Day (last 7 days):");
|
|
2997
|
+
lines.push("\u2500".repeat(50));
|
|
2998
|
+
const maxDayCount = Math.max(...stats.incidents.byDay.map((d) => d.count), 1);
|
|
2999
|
+
for (const day of stats.incidents.byDay.slice(-7)) {
|
|
3000
|
+
const barLength = Math.round(day.count / maxDayCount * 30);
|
|
3001
|
+
const bar = "\u2588".repeat(barLength);
|
|
3002
|
+
lines.push(`${day.date.substring(5)} ${bar} ${day.count}`);
|
|
3003
|
+
}
|
|
3004
|
+
lines.push("");
|
|
3005
|
+
lines.push("Most Affected Symbols:");
|
|
3006
|
+
lines.push("\u2500".repeat(50));
|
|
3007
|
+
for (const { symbol, count } of stats.symbols.mostIncidents.slice(0, 5)) {
|
|
3008
|
+
lines.push(` ${symbol.padEnd(25)} ${count} incidents`);
|
|
3009
|
+
}
|
|
3010
|
+
lines.push("");
|
|
3011
|
+
lines.push("Top Patterns:");
|
|
3012
|
+
lines.push("\u2500".repeat(50));
|
|
3013
|
+
for (const { patternId, resolvedCount } of stats.patterns.mostEffective.slice(0, 5)) {
|
|
3014
|
+
lines.push(` ${patternId.padEnd(25)} ${resolvedCount} resolved`);
|
|
3015
|
+
}
|
|
3016
|
+
lines.push("");
|
|
3017
|
+
lines.push("Resolution Stats:");
|
|
3018
|
+
lines.push("\u2500".repeat(50));
|
|
3019
|
+
lines.push(` Resolution rate: ${Math.round(stats.resolution.resolutionRate)}%`);
|
|
3020
|
+
lines.push(` With pattern: ${stats.resolution.resolvedWithPattern}`);
|
|
3021
|
+
lines.push(` Manual: ${stats.resolution.resolvedManually}`);
|
|
3022
|
+
return lines.join("\n");
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Helper: Count symbols across incidents
|
|
3026
|
+
*/
|
|
3027
|
+
countSymbols(incidents) {
|
|
3028
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3029
|
+
for (const incident of incidents) {
|
|
3030
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
3031
|
+
if (value) {
|
|
3032
|
+
counts.set(value, (counts.get(value) || 0) + 1);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return counts;
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Helper: Get all symbols from incident
|
|
3040
|
+
*/
|
|
3041
|
+
getSymbolsFromIncident(incident) {
|
|
3042
|
+
const symbols = [];
|
|
3043
|
+
for (const [, value] of Object.entries(incident.symbols)) {
|
|
3044
|
+
if (value) {
|
|
3045
|
+
symbols.push(value);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
return symbols;
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
|
|
3052
|
+
// ../sentinel/src/suggester.ts
|
|
3053
|
+
var PatternSuggester = class {
|
|
3054
|
+
constructor(storage2) {
|
|
3055
|
+
this.storage = storage2;
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Suggest a pattern from a resolved incident
|
|
3059
|
+
*/
|
|
3060
|
+
suggestFromIncident(incident) {
|
|
3061
|
+
const baseId = this.generatePatternId(incident);
|
|
3062
|
+
const symbols = this.buildSymbolCriteria(incident.symbols);
|
|
3063
|
+
const errorKeywords = this.extractErrorKeywords(incident.error.message);
|
|
3064
|
+
const pattern = {
|
|
3065
|
+
id: baseId,
|
|
3066
|
+
name: this.generatePatternName(incident),
|
|
3067
|
+
description: `Auto-suggested pattern from incident ${incident.id}`,
|
|
3068
|
+
pattern: {
|
|
3069
|
+
symbols,
|
|
3070
|
+
errorContains: errorKeywords.length > 0 ? errorKeywords : void 0,
|
|
3071
|
+
missingSignals: incident.flowPosition?.missing
|
|
3072
|
+
},
|
|
3073
|
+
resolution: {
|
|
3074
|
+
description: incident.resolution?.notes || "Resolution approach TBD",
|
|
3075
|
+
strategy: this.inferStrategy([incident]),
|
|
3076
|
+
priority: "medium"
|
|
3077
|
+
},
|
|
3078
|
+
source: "suggested",
|
|
3079
|
+
private: false,
|
|
3080
|
+
tags: this.generateTags(incident)
|
|
3081
|
+
};
|
|
3082
|
+
return pattern;
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Suggest a pattern from an incident group
|
|
3086
|
+
*/
|
|
3087
|
+
suggestFromGroup(group) {
|
|
3088
|
+
const baseId = `group-${group.id.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
|
3089
|
+
const symbols = this.buildSymbolCriteria(group.commonSymbols);
|
|
3090
|
+
const groupIncidents = group.incidents.slice(0, 20).map((id) => this.storage.getIncident(id)).filter((i) => i != null);
|
|
3091
|
+
const pattern = {
|
|
3092
|
+
id: baseId,
|
|
3093
|
+
name: group.name || `Pattern from group ${group.id}`,
|
|
3094
|
+
description: `Auto-suggested pattern from incident group with ${group.count} incidents`,
|
|
3095
|
+
pattern: {
|
|
3096
|
+
symbols,
|
|
3097
|
+
errorContains: group.commonErrorPatterns.length > 0 ? group.commonErrorPatterns : void 0
|
|
3098
|
+
},
|
|
3099
|
+
resolution: {
|
|
3100
|
+
description: "Resolution approach TBD based on grouped incidents",
|
|
3101
|
+
strategy: groupIncidents.length > 0 ? this.inferStrategy(groupIncidents) : "fix-code",
|
|
3102
|
+
priority: this.getPriorityFromCount(group.count)
|
|
3103
|
+
},
|
|
3104
|
+
source: "suggested",
|
|
3105
|
+
private: false,
|
|
3106
|
+
tags: this.generateTagsFromGroup(group)
|
|
3107
|
+
};
|
|
3108
|
+
return pattern;
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Find incidents that could become patterns
|
|
3112
|
+
*/
|
|
3113
|
+
findPatternCandidates(minOccurrences = 3) {
|
|
3114
|
+
const incidents = this.storage.getRecentIncidents({
|
|
3115
|
+
limit: 1e3,
|
|
3116
|
+
status: "resolved"
|
|
3117
|
+
});
|
|
3118
|
+
const signatureGroups = /* @__PURE__ */ new Map();
|
|
3119
|
+
for (const incident of incidents) {
|
|
3120
|
+
const signature = this.getSymbolSignature(incident.symbols);
|
|
3121
|
+
const existing = signatureGroups.get(signature) || [];
|
|
3122
|
+
existing.push(incident);
|
|
3123
|
+
signatureGroups.set(signature, existing);
|
|
3124
|
+
}
|
|
3125
|
+
const candidates = [];
|
|
3126
|
+
for (const [, groupIncidents] of signatureGroups) {
|
|
3127
|
+
if (groupIncidents.length >= minOccurrences) {
|
|
3128
|
+
const hasPattern = this.hasMatchingPattern(groupIncidents[0]);
|
|
3129
|
+
if (hasPattern) continue;
|
|
3130
|
+
const suggestedPattern = this.suggestFromIncidents(groupIncidents);
|
|
3131
|
+
candidates.push({
|
|
3132
|
+
incidents: groupIncidents,
|
|
3133
|
+
suggestedPattern,
|
|
3134
|
+
occurrenceCount: groupIncidents.length
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
return candidates.sort((a, b) => b.occurrenceCount - a.occurrenceCount);
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* Generate pattern from multiple similar incidents
|
|
3142
|
+
*/
|
|
3143
|
+
suggestFromIncidents(incidents) {
|
|
3144
|
+
const commonSymbols = this.extractCommonSymbols(incidents);
|
|
3145
|
+
const symbols = this.buildSymbolCriteria(commonSymbols);
|
|
3146
|
+
const errorKeywords = this.extractCommonErrorKeywords(incidents);
|
|
3147
|
+
const missingSignals = this.extractCommonMissingSignals(incidents);
|
|
3148
|
+
const baseId = this.generatePatternId(incidents[0]);
|
|
3149
|
+
return {
|
|
3150
|
+
id: baseId,
|
|
3151
|
+
name: this.generatePatternName(incidents[0]),
|
|
3152
|
+
description: `Auto-suggested pattern from ${incidents.length} similar incidents`,
|
|
3153
|
+
pattern: {
|
|
3154
|
+
symbols,
|
|
3155
|
+
errorContains: errorKeywords.length > 0 ? errorKeywords : void 0,
|
|
3156
|
+
missingSignals: missingSignals.length > 0 ? missingSignals : void 0
|
|
3157
|
+
},
|
|
3158
|
+
resolution: {
|
|
3159
|
+
description: "Resolution approach based on previous resolutions",
|
|
3160
|
+
strategy: this.inferStrategy(incidents),
|
|
3161
|
+
priority: this.getPriorityFromCount(incidents.length)
|
|
3162
|
+
},
|
|
3163
|
+
source: "suggested",
|
|
3164
|
+
private: false,
|
|
3165
|
+
tags: this.generateTagsFromIncidents(incidents)
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Build symbol criteria for pattern, adding wildcards where appropriate
|
|
3170
|
+
*/
|
|
3171
|
+
buildSymbolCriteria(symbols) {
|
|
3172
|
+
const criteria = {};
|
|
3173
|
+
for (const [key, value] of Object.entries(symbols)) {
|
|
3174
|
+
if (value) {
|
|
3175
|
+
criteria[key] = value;
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
return criteria;
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Extract keywords from error message
|
|
3182
|
+
*/
|
|
3183
|
+
extractErrorKeywords(message) {
|
|
3184
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
3185
|
+
"the",
|
|
3186
|
+
"a",
|
|
3187
|
+
"an",
|
|
3188
|
+
"is",
|
|
3189
|
+
"are",
|
|
3190
|
+
"was",
|
|
3191
|
+
"were",
|
|
3192
|
+
"in",
|
|
3193
|
+
"on",
|
|
3194
|
+
"at",
|
|
3195
|
+
"to",
|
|
3196
|
+
"for",
|
|
3197
|
+
"of",
|
|
3198
|
+
"with",
|
|
3199
|
+
"and",
|
|
3200
|
+
"or",
|
|
3201
|
+
"but",
|
|
3202
|
+
"not",
|
|
3203
|
+
"no",
|
|
3204
|
+
"be",
|
|
3205
|
+
"been",
|
|
3206
|
+
"have",
|
|
3207
|
+
"has",
|
|
3208
|
+
"had",
|
|
3209
|
+
"do",
|
|
3210
|
+
"does",
|
|
3211
|
+
"did"
|
|
3212
|
+
]);
|
|
3213
|
+
const words = message.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
3214
|
+
const unique = [...new Set(words)];
|
|
3215
|
+
return unique.slice(0, 5);
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Extract common error keywords from multiple incidents
|
|
3219
|
+
*/
|
|
3220
|
+
extractCommonErrorKeywords(incidents) {
|
|
3221
|
+
const wordCounts = /* @__PURE__ */ new Map();
|
|
3222
|
+
for (const incident of incidents) {
|
|
3223
|
+
const keywords = this.extractErrorKeywords(incident.error.message);
|
|
3224
|
+
for (const keyword of keywords) {
|
|
3225
|
+
wordCounts.set(keyword, (wordCounts.get(keyword) || 0) + 1);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
const threshold = Math.ceil(incidents.length * 0.5);
|
|
3229
|
+
return Array.from(wordCounts.entries()).filter(([, count]) => count >= threshold).map(([word]) => word).slice(0, 5);
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Extract symbols common to all incidents
|
|
3233
|
+
*/
|
|
3234
|
+
extractCommonSymbols(incidents) {
|
|
3235
|
+
if (incidents.length === 0) return {};
|
|
3236
|
+
const first = incidents[0].symbols;
|
|
3237
|
+
const common = {};
|
|
3238
|
+
for (const [key, value] of Object.entries(first)) {
|
|
3239
|
+
if (!value) continue;
|
|
3240
|
+
const allMatch = incidents.every(
|
|
3241
|
+
(i) => i.symbols[key] === value
|
|
3242
|
+
);
|
|
3243
|
+
if (allMatch) {
|
|
3244
|
+
common[key] = value;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return common;
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* Extract missing signals common to multiple incidents
|
|
3251
|
+
*/
|
|
3252
|
+
extractCommonMissingSignals(incidents) {
|
|
3253
|
+
const signalCounts = /* @__PURE__ */ new Map();
|
|
3254
|
+
for (const incident of incidents) {
|
|
3255
|
+
if (!incident.flowPosition?.missing) continue;
|
|
3256
|
+
for (const signal of incident.flowPosition.missing) {
|
|
3257
|
+
signalCounts.set(signal, (signalCounts.get(signal) || 0) + 1);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
const threshold = Math.ceil(incidents.length * 0.5);
|
|
3261
|
+
return Array.from(signalCounts.entries()).filter(([, count]) => count >= threshold).map(([signal]) => signal);
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* Generate a pattern ID from incident
|
|
3265
|
+
*/
|
|
3266
|
+
generatePatternId(incident) {
|
|
3267
|
+
const parts = [];
|
|
3268
|
+
if (incident.symbols.gate) {
|
|
3269
|
+
parts.push(incident.symbols.gate.replace(/[^a-z0-9]/gi, ""));
|
|
3270
|
+
} else if (incident.symbols.feature) {
|
|
3271
|
+
parts.push(incident.symbols.feature.replace(/[^a-z0-9]/gi, ""));
|
|
3272
|
+
} else if (incident.symbols.component) {
|
|
3273
|
+
parts.push(incident.symbols.component.replace(/[^a-z0-9]/gi, ""));
|
|
3274
|
+
} else if (incident.symbols.integration) {
|
|
3275
|
+
parts.push(incident.symbols.integration.replace(/[^a-z0-9]/gi, ""));
|
|
3276
|
+
} else {
|
|
3277
|
+
parts.push("unknown");
|
|
3278
|
+
}
|
|
3279
|
+
const errorType = incident.error.type?.toLowerCase() || "error";
|
|
3280
|
+
parts.push(errorType.replace(/[^a-z0-9]/gi, ""));
|
|
3281
|
+
parts.push(String(Date.now() % 1e3).padStart(3, "0"));
|
|
3282
|
+
return parts.join("-");
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Generate a human-readable pattern name
|
|
3286
|
+
*/
|
|
3287
|
+
generatePatternName(incident) {
|
|
3288
|
+
const parts = [];
|
|
3289
|
+
if (incident.symbols.feature) {
|
|
3290
|
+
parts.push(
|
|
3291
|
+
incident.symbols.feature.replace("@", "").replace(/-/g, " ")
|
|
3292
|
+
);
|
|
3293
|
+
}
|
|
3294
|
+
if (incident.symbols.gate) {
|
|
3295
|
+
parts.push("gate " + incident.symbols.gate.replace("^", ""));
|
|
3296
|
+
}
|
|
3297
|
+
if (incident.error.type) {
|
|
3298
|
+
parts.push(incident.error.type);
|
|
3299
|
+
}
|
|
3300
|
+
if (parts.length === 0) {
|
|
3301
|
+
return "Unnamed Pattern";
|
|
3302
|
+
}
|
|
3303
|
+
const name = parts.join(" - ");
|
|
3304
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Generate tags from incident
|
|
3308
|
+
*/
|
|
3309
|
+
generateTags(incident) {
|
|
3310
|
+
const tags = [];
|
|
3311
|
+
if (incident.symbols.feature) {
|
|
3312
|
+
tags.push("feature");
|
|
3313
|
+
}
|
|
3314
|
+
if (incident.symbols.gate) {
|
|
3315
|
+
tags.push("gate");
|
|
3316
|
+
}
|
|
3317
|
+
if (incident.symbols.integration) {
|
|
3318
|
+
tags.push("integration");
|
|
3319
|
+
tags.push(incident.symbols.integration.replace("&", ""));
|
|
3320
|
+
}
|
|
3321
|
+
if (incident.error.type) {
|
|
3322
|
+
tags.push(incident.error.type.toLowerCase());
|
|
3323
|
+
}
|
|
3324
|
+
tags.push(incident.environment);
|
|
3325
|
+
return [...new Set(tags)].slice(0, 5);
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Generate tags from incident group
|
|
3329
|
+
*/
|
|
3330
|
+
generateTagsFromGroup(group) {
|
|
3331
|
+
const tags = ["grouped"];
|
|
3332
|
+
if (group.commonSymbols.feature) {
|
|
3333
|
+
tags.push("feature");
|
|
3334
|
+
}
|
|
3335
|
+
if (group.commonSymbols.gate) {
|
|
3336
|
+
tags.push("gate");
|
|
3337
|
+
}
|
|
3338
|
+
if (group.commonSymbols.integration) {
|
|
3339
|
+
tags.push("integration");
|
|
3340
|
+
}
|
|
3341
|
+
for (const env of group.environments) {
|
|
3342
|
+
tags.push(env);
|
|
3343
|
+
}
|
|
3344
|
+
return [...new Set(tags)].slice(0, 5);
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* Generate tags from multiple incidents
|
|
3348
|
+
*/
|
|
3349
|
+
generateTagsFromIncidents(incidents) {
|
|
3350
|
+
const tagCounts = /* @__PURE__ */ new Map();
|
|
3351
|
+
for (const incident of incidents) {
|
|
3352
|
+
const tags = this.generateTags(incident);
|
|
3353
|
+
for (const tag of tags) {
|
|
3354
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
return Array.from(tagCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([tag]) => tag);
|
|
3358
|
+
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Get symbol signature for grouping
|
|
3361
|
+
*/
|
|
3362
|
+
getSymbolSignature(symbols) {
|
|
3363
|
+
const parts = [];
|
|
3364
|
+
if (symbols.feature) parts.push(`f:${symbols.feature}`);
|
|
3365
|
+
if (symbols.component) parts.push(`c:${symbols.component}`);
|
|
3366
|
+
if (symbols.flow) parts.push(`fl:${symbols.flow}`);
|
|
3367
|
+
if (symbols.gate) parts.push(`g:${symbols.gate}`);
|
|
3368
|
+
if (symbols.integration) parts.push(`i:${symbols.integration}`);
|
|
3369
|
+
return parts.sort().join("|");
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Check if there's already a pattern matching this incident
|
|
3373
|
+
*/
|
|
3374
|
+
hasMatchingPattern(incident) {
|
|
3375
|
+
const patterns = this.storage.getAllPatterns({ includePrivate: true });
|
|
3376
|
+
for (const pattern of patterns) {
|
|
3377
|
+
let matchCount = 0;
|
|
3378
|
+
const symbolTypes = [
|
|
3379
|
+
"feature",
|
|
3380
|
+
"component",
|
|
3381
|
+
"flow",
|
|
3382
|
+
"gate",
|
|
3383
|
+
"signal",
|
|
3384
|
+
"integration"
|
|
3385
|
+
];
|
|
3386
|
+
for (const type of symbolTypes) {
|
|
3387
|
+
const patternValue = pattern.pattern.symbols[type];
|
|
3388
|
+
const incidentValue = incident.symbols[type];
|
|
3389
|
+
if (patternValue && incidentValue && patternValue === incidentValue) {
|
|
3390
|
+
matchCount++;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
if (matchCount >= 2) {
|
|
3394
|
+
return true;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
return false;
|
|
3398
|
+
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Infer resolution strategy from incident error patterns and context.
|
|
3401
|
+
* Uses keyword heuristics across all incident messages to pick the
|
|
3402
|
+
* most likely resolution approach.
|
|
3403
|
+
*/
|
|
3404
|
+
inferStrategy(incidents) {
|
|
3405
|
+
const messages = incidents.map((i) => i.error.message.toLowerCase());
|
|
3406
|
+
const hasKeyword = (keywords) => messages.some((m) => keywords.some((k) => m.includes(k)));
|
|
3407
|
+
if (hasKeyword(["revert", "rollback", "regression", "broke after deploy", "since deploy"])) {
|
|
3408
|
+
return "rollback";
|
|
3409
|
+
}
|
|
3410
|
+
if (hasKeyword(["config", "environment variable", "env var", "missing key", "secret", "credential"])) {
|
|
3411
|
+
return "config-change";
|
|
3412
|
+
}
|
|
3413
|
+
if (hasKeyword(["out of memory", "oom", "heap", "memory limit", "capacity", "too many connections", "pool exhausted"])) {
|
|
3414
|
+
return "scale-up";
|
|
3415
|
+
}
|
|
3416
|
+
if (hasKeyword(["timeout", "network", "econnrefused", "econnreset", "dns", "socket hang up"])) {
|
|
3417
|
+
return "retry";
|
|
3418
|
+
}
|
|
3419
|
+
if (hasKeyword(["unavailable", "service down", "circuit breaker", "fallback", "503", "502"])) {
|
|
3420
|
+
return "fallback";
|
|
3421
|
+
}
|
|
3422
|
+
if (hasKeyword(["validation", "invalid", "required", "constraint", "duplicate", "not found", "404"])) {
|
|
3423
|
+
return "fix-data";
|
|
3424
|
+
}
|
|
3425
|
+
if (hasKeyword(["permission", "forbidden", "403", "401", "unauthorized", "access denied"])) {
|
|
3426
|
+
return "escalate";
|
|
3427
|
+
}
|
|
3428
|
+
const uniqueTypes = new Set(incidents.map((i) => i.error.type).filter(Boolean));
|
|
3429
|
+
if (uniqueTypes.size > 2) {
|
|
3430
|
+
return "investigate";
|
|
3431
|
+
}
|
|
3432
|
+
return "fix-code";
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Get priority based on occurrence count
|
|
3436
|
+
*/
|
|
3437
|
+
getPriorityFromCount(count) {
|
|
3438
|
+
if (count >= 20) return "critical";
|
|
3439
|
+
if (count >= 10) return "high";
|
|
3440
|
+
if (count >= 5) return "medium";
|
|
3441
|
+
return "low";
|
|
3442
|
+
}
|
|
3443
|
+
};
|
|
3444
|
+
|
|
3445
|
+
// ../sentinel/src/seeds/loader.ts
|
|
3446
|
+
import * as path2 from "path";
|
|
3447
|
+
import * as fs2 from "fs";
|
|
3448
|
+
import { fileURLToPath } from "url";
|
|
3449
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3450
|
+
var __dirname = path2.dirname(__filename);
|
|
3451
|
+
function loadUniversalPatterns() {
|
|
3452
|
+
const filePath = path2.join(__dirname, "universal-patterns.json");
|
|
3453
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
3454
|
+
return JSON.parse(content);
|
|
3455
|
+
}
|
|
3456
|
+
function loadParadigmPatterns() {
|
|
3457
|
+
const filePath = path2.join(__dirname, "paradigm-patterns.json");
|
|
3458
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
3459
|
+
return JSON.parse(content);
|
|
3460
|
+
}
|
|
3461
|
+
function loadAllSeedPatterns() {
|
|
3462
|
+
const universal = loadUniversalPatterns();
|
|
3463
|
+
const paradigm = loadParadigmPatterns();
|
|
3464
|
+
return {
|
|
3465
|
+
version: "1.0.0",
|
|
3466
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3467
|
+
patterns: [...universal.patterns, ...paradigm.patterns]
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// ../sentinel/src/mcp.ts
|
|
3472
|
+
var storage = null;
|
|
3473
|
+
var initialized = false;
|
|
3474
|
+
function getStorage() {
|
|
3475
|
+
if (!storage) {
|
|
3476
|
+
storage = new SentinelStorage();
|
|
3477
|
+
}
|
|
3478
|
+
if (!initialized) {
|
|
3479
|
+
try {
|
|
3480
|
+
const { patterns } = loadAllSeedPatterns();
|
|
3481
|
+
for (const pattern of patterns) {
|
|
3482
|
+
try {
|
|
3483
|
+
storage.addPattern(pattern);
|
|
3484
|
+
} catch {
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
} catch {
|
|
3488
|
+
}
|
|
3489
|
+
initialized = true;
|
|
3490
|
+
}
|
|
3491
|
+
return storage;
|
|
3492
|
+
}
|
|
3493
|
+
function getToolsList() {
|
|
3494
|
+
return [
|
|
3495
|
+
{
|
|
3496
|
+
name: "sentinel_triage",
|
|
3497
|
+
description: "View and filter incidents with pattern matches. Returns recent errors with symbolic context and resolution suggestions.",
|
|
3498
|
+
annotations: {
|
|
3499
|
+
readOnlyHint: true,
|
|
3500
|
+
destructiveHint: false
|
|
3501
|
+
},
|
|
3502
|
+
inputSchema: {
|
|
3503
|
+
type: "object",
|
|
3504
|
+
properties: {
|
|
3505
|
+
symbol: { type: "string", description: "Filter by symbol (e.g., #checkout, ^auth)" },
|
|
3506
|
+
status: {
|
|
3507
|
+
type: "string",
|
|
3508
|
+
enum: ["open", "investigating", "resolved", "wont-fix", "all"],
|
|
3509
|
+
description: "Filter by status (default: all)"
|
|
3510
|
+
},
|
|
3511
|
+
environment: { type: "string", description: "Filter by environment" },
|
|
3512
|
+
search: { type: "string", description: "Search in error messages" },
|
|
3513
|
+
limit: { type: "number", description: "Max results (default: 10)" }
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
},
|
|
3517
|
+
{
|
|
3518
|
+
name: "sentinel_show",
|
|
3519
|
+
description: "Get full details of a specific incident including timeline and matched patterns.",
|
|
3520
|
+
annotations: {
|
|
3521
|
+
readOnlyHint: true,
|
|
3522
|
+
destructiveHint: false
|
|
3523
|
+
},
|
|
3524
|
+
inputSchema: {
|
|
3525
|
+
type: "object",
|
|
3526
|
+
properties: {
|
|
3527
|
+
incidentId: { type: "string", description: "Incident ID (e.g., INC-001)" },
|
|
3528
|
+
includeTimeline: { type: "boolean", description: "Include flow timeline" },
|
|
3529
|
+
includeSimilar: { type: "boolean", description: "Include similar incidents" }
|
|
3530
|
+
},
|
|
3531
|
+
required: ["incidentId"]
|
|
3532
|
+
}
|
|
3533
|
+
},
|
|
3534
|
+
{
|
|
3535
|
+
name: "sentinel_resolve",
|
|
3536
|
+
description: "Mark an incident as resolved with optional pattern and commit reference.",
|
|
3537
|
+
annotations: {
|
|
3538
|
+
readOnlyHint: false,
|
|
3539
|
+
destructiveHint: false
|
|
3540
|
+
},
|
|
3541
|
+
inputSchema: {
|
|
3542
|
+
type: "object",
|
|
3543
|
+
properties: {
|
|
3544
|
+
incidentId: { type: "string", description: "Incident ID" },
|
|
3545
|
+
patternId: { type: "string", description: "Pattern that led to resolution" },
|
|
3546
|
+
commitHash: { type: "string", description: "Fix commit hash" },
|
|
3547
|
+
prUrl: { type: "string", description: "PR URL" },
|
|
3548
|
+
notes: { type: "string", description: "Resolution notes" },
|
|
3549
|
+
wontFix: { type: "boolean", description: "Mark as wont-fix instead of resolved" }
|
|
3550
|
+
},
|
|
3551
|
+
required: ["incidentId"]
|
|
3552
|
+
}
|
|
3553
|
+
},
|
|
3554
|
+
{
|
|
3555
|
+
name: "sentinel_patterns",
|
|
3556
|
+
description: "List and filter failure patterns with confidence scores.",
|
|
3557
|
+
annotations: {
|
|
3558
|
+
readOnlyHint: true,
|
|
3559
|
+
destructiveHint: false
|
|
3560
|
+
},
|
|
3561
|
+
inputSchema: {
|
|
3562
|
+
type: "object",
|
|
3563
|
+
properties: {
|
|
3564
|
+
symbol: { type: "string", description: "Filter patterns for this symbol" },
|
|
3565
|
+
minConfidence: { type: "number", description: "Minimum confidence score" },
|
|
3566
|
+
source: {
|
|
3567
|
+
type: "string",
|
|
3568
|
+
enum: ["manual", "suggested", "imported", "community"],
|
|
3569
|
+
description: "Filter by source"
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
},
|
|
3574
|
+
{
|
|
3575
|
+
name: "sentinel_add_pattern",
|
|
3576
|
+
description: "Create a new failure pattern.",
|
|
3577
|
+
annotations: {
|
|
3578
|
+
readOnlyHint: false,
|
|
3579
|
+
destructiveHint: false
|
|
3580
|
+
},
|
|
3581
|
+
inputSchema: {
|
|
3582
|
+
type: "object",
|
|
3583
|
+
properties: {
|
|
3584
|
+
id: { type: "string", description: "Pattern ID (kebab-case)" },
|
|
3585
|
+
name: { type: "string", description: "Human-readable name" },
|
|
3586
|
+
description: { type: "string", description: "What this pattern matches" },
|
|
3587
|
+
pattern: {
|
|
3588
|
+
type: "object",
|
|
3589
|
+
properties: {
|
|
3590
|
+
symbols: { type: "object", description: "Symbol criteria" },
|
|
3591
|
+
errorContains: { type: "array", items: { type: "string" }, description: "Error keywords" },
|
|
3592
|
+
missingSignals: { type: "array", items: { type: "string" }, description: "Expected missing signals" }
|
|
3593
|
+
}
|
|
3594
|
+
},
|
|
3595
|
+
resolution: {
|
|
3596
|
+
type: "object",
|
|
3597
|
+
properties: {
|
|
3598
|
+
description: { type: "string", description: "Resolution steps" },
|
|
3599
|
+
strategy: {
|
|
3600
|
+
type: "string",
|
|
3601
|
+
enum: ["retry", "fallback", "fix-data", "fix-code", "ignore", "escalate"]
|
|
3602
|
+
},
|
|
3603
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
3604
|
+
codeHint: { type: "string", description: "Code fix hint" }
|
|
3605
|
+
},
|
|
3606
|
+
required: ["description", "strategy"]
|
|
3607
|
+
},
|
|
3608
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" }
|
|
3609
|
+
},
|
|
3610
|
+
required: ["id", "name", "pattern", "resolution"]
|
|
3611
|
+
}
|
|
3612
|
+
},
|
|
3613
|
+
{
|
|
3614
|
+
name: "sentinel_record",
|
|
3615
|
+
description: "Manually record a new incident.",
|
|
3616
|
+
annotations: {
|
|
3617
|
+
readOnlyHint: false,
|
|
3618
|
+
destructiveHint: false
|
|
3619
|
+
},
|
|
3620
|
+
inputSchema: {
|
|
3621
|
+
type: "object",
|
|
3622
|
+
properties: {
|
|
3623
|
+
error: {
|
|
3624
|
+
type: "object",
|
|
3625
|
+
properties: {
|
|
3626
|
+
message: { type: "string", description: "Error message" },
|
|
3627
|
+
stack: { type: "string", description: "Stack trace" },
|
|
3628
|
+
code: { type: "string", description: "Error code" },
|
|
3629
|
+
type: { type: "string", description: "Error type" }
|
|
3630
|
+
},
|
|
3631
|
+
required: ["message"]
|
|
3632
|
+
},
|
|
3633
|
+
symbols: {
|
|
3634
|
+
type: "object",
|
|
3635
|
+
properties: {
|
|
3636
|
+
component: { type: "string" },
|
|
3637
|
+
flow: { type: "string" },
|
|
3638
|
+
gate: { type: "string" },
|
|
3639
|
+
signal: { type: "string" }
|
|
3640
|
+
}
|
|
3641
|
+
},
|
|
3642
|
+
environment: { type: "string", description: "Environment (required)" },
|
|
3643
|
+
service: { type: "string", description: "Service name" },
|
|
3644
|
+
version: { type: "string", description: "App version" },
|
|
3645
|
+
flowPosition: {
|
|
3646
|
+
type: "object",
|
|
3647
|
+
properties: {
|
|
3648
|
+
flowId: { type: "string" },
|
|
3649
|
+
expected: { type: "array", items: { type: "string" } },
|
|
3650
|
+
actual: { type: "array", items: { type: "string" } },
|
|
3651
|
+
missing: { type: "array", items: { type: "string" } },
|
|
3652
|
+
failedAt: { type: "string" }
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
},
|
|
3656
|
+
required: ["error", "symbols", "environment"]
|
|
3657
|
+
}
|
|
3658
|
+
},
|
|
3659
|
+
{
|
|
3660
|
+
name: "sentinel_stats",
|
|
3661
|
+
description: "Get statistics and health metrics.",
|
|
3662
|
+
annotations: {
|
|
3663
|
+
readOnlyHint: true,
|
|
3664
|
+
destructiveHint: false
|
|
3665
|
+
},
|
|
3666
|
+
inputSchema: {
|
|
3667
|
+
type: "object",
|
|
3668
|
+
properties: {
|
|
3669
|
+
period: { type: "string", description: "Period: 1d, 7d, 30d, 90d (default: 7d)" },
|
|
3670
|
+
symbol: { type: "string", description: "Get health for specific symbol" }
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
},
|
|
3674
|
+
{
|
|
3675
|
+
name: "sentinel_suggest_pattern",
|
|
3676
|
+
description: "Get AI suggestions for patterns based on incidents.",
|
|
3677
|
+
annotations: {
|
|
3678
|
+
readOnlyHint: true,
|
|
3679
|
+
destructiveHint: false
|
|
3680
|
+
},
|
|
3681
|
+
inputSchema: {
|
|
3682
|
+
type: "object",
|
|
3683
|
+
properties: {
|
|
3684
|
+
incidentId: { type: "string", description: "Suggest from specific incident" },
|
|
3685
|
+
minOccurrences: { type: "number", description: "Min similar incidents for suggestion" }
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
},
|
|
3689
|
+
// ─── Observability Tools ──────────────────────────────────────
|
|
3690
|
+
{
|
|
3691
|
+
name: "sentinel_logs",
|
|
3692
|
+
description: "Query structured logs from connected apps. Filters by level, symbol, service, search text, time range.",
|
|
3693
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3694
|
+
inputSchema: {
|
|
3695
|
+
type: "object",
|
|
3696
|
+
properties: {
|
|
3697
|
+
level: { type: "string", enum: ["debug", "info", "warn", "error"], description: "Filter by log level" },
|
|
3698
|
+
symbol: { type: "string", description: "Filter by symbol (partial match)" },
|
|
3699
|
+
service: { type: "string", description: "Filter by service name" },
|
|
3700
|
+
search: { type: "string", description: "Search in log messages" },
|
|
3701
|
+
since: { type: "string", description: "ISO timestamp \u2014 logs after this time" },
|
|
3702
|
+
sessionId: { type: "string", description: "Filter by session ID" },
|
|
3703
|
+
correlationId: { type: "string", description: "Filter by correlation ID" },
|
|
3704
|
+
limit: { type: "number", description: "Max results (default: 50)" }
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
},
|
|
3708
|
+
{
|
|
3709
|
+
name: "sentinel_services",
|
|
3710
|
+
description: "List all registered services with version, environment, and last-seen time.",
|
|
3711
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3712
|
+
inputSchema: {
|
|
3713
|
+
type: "object",
|
|
3714
|
+
properties: {}
|
|
3715
|
+
}
|
|
3716
|
+
},
|
|
3717
|
+
{
|
|
3718
|
+
name: "sentinel_app_state",
|
|
3719
|
+
description: "Get live app state snapshots. Shows current state, active flows, and held gates for connected services.",
|
|
3720
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3721
|
+
inputSchema: {
|
|
3722
|
+
type: "object",
|
|
3723
|
+
properties: {
|
|
3724
|
+
service: { type: "string", description: "Filter by service name" }
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
},
|
|
3728
|
+
{
|
|
3729
|
+
name: "sentinel_validate_symbol",
|
|
3730
|
+
description: "Check if a symbol has been used in logs. Returns usage count and suggestions.",
|
|
3731
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3732
|
+
inputSchema: {
|
|
3733
|
+
type: "object",
|
|
3734
|
+
properties: {
|
|
3735
|
+
symbol: { type: "string", description: "Symbol to validate (e.g., #checkout, ^auth)" }
|
|
3736
|
+
},
|
|
3737
|
+
required: ["symbol"]
|
|
3738
|
+
}
|
|
3739
|
+
},
|
|
3740
|
+
{
|
|
3741
|
+
name: "sentinel_flow_activity",
|
|
3742
|
+
description: "Get recent flow events \u2014 which flow nodes were hit, in what order, by which service.",
|
|
3743
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3744
|
+
inputSchema: {
|
|
3745
|
+
type: "object",
|
|
3746
|
+
properties: {
|
|
3747
|
+
flowId: { type: "string", description: "Filter by flow ID (e.g., $checkout-flow)" },
|
|
3748
|
+
service: { type: "string", description: "Filter by service name" },
|
|
3749
|
+
since: { type: "string", description: "ISO timestamp \u2014 events after this time" }
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
name: "sentinel_metrics",
|
|
3755
|
+
description: "Query metrics (counters, gauges, histograms) from connected apps. Supports filtering and aggregation.",
|
|
3756
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3757
|
+
inputSchema: {
|
|
3758
|
+
type: "object",
|
|
3759
|
+
properties: {
|
|
3760
|
+
name: { type: "string", description: "Metric name filter" },
|
|
3761
|
+
type: { type: "string", enum: ["counter", "gauge", "histogram"], description: "Metric type filter" },
|
|
3762
|
+
service: { type: "string", description: "Service name filter" },
|
|
3763
|
+
since: { type: "string", description: "ISO timestamp \u2014 metrics after this time" },
|
|
3764
|
+
aggregate: { type: "boolean", description: "If true and name is provided, return aggregation instead of raw data" },
|
|
3765
|
+
limit: { type: "number", description: "Max results (default: 50)" }
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
},
|
|
3769
|
+
{
|
|
3770
|
+
name: "sentinel_traces",
|
|
3771
|
+
description: "Query distributed traces across services. Shows span trees with timing, status, and service hops.",
|
|
3772
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
3773
|
+
inputSchema: {
|
|
3774
|
+
type: "object",
|
|
3775
|
+
properties: {
|
|
3776
|
+
traceId: { type: "string", description: "Get a specific trace by ID" },
|
|
3777
|
+
service: { type: "string", description: "Filter by service name" },
|
|
3778
|
+
symbol: { type: "string", description: "Filter by symbol" },
|
|
3779
|
+
since: { type: "string", description: "ISO timestamp \u2014 traces after this time" },
|
|
3780
|
+
limit: { type: "number", description: "Max traces (default: 10, max: 20)" }
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
];
|
|
3785
|
+
}
|
|
3786
|
+
async function handleTool(name, args) {
|
|
3787
|
+
const store = getStorage();
|
|
3788
|
+
const matcher = new PatternMatcher(store);
|
|
3789
|
+
switch (name) {
|
|
3790
|
+
case "sentinel_triage": {
|
|
3791
|
+
const { symbol, status = "all", environment, search, limit = 10 } = args;
|
|
3792
|
+
const incidents = store.getRecentIncidents({
|
|
3793
|
+
limit,
|
|
3794
|
+
status,
|
|
3795
|
+
symbol,
|
|
3796
|
+
environment,
|
|
3797
|
+
search
|
|
3798
|
+
});
|
|
3799
|
+
if (incidents.length === 0) {
|
|
3800
|
+
return JSON.stringify({
|
|
3801
|
+
count: 0,
|
|
3802
|
+
incidents: [],
|
|
3803
|
+
tip: "No incidents recorded yet. Use sentinel_record to create incidents or integrate the SDK."
|
|
3804
|
+
}, null, 2);
|
|
3805
|
+
}
|
|
3806
|
+
const results = incidents.map((incident) => {
|
|
3807
|
+
const matches = matcher.match(incident, { maxResults: 3 });
|
|
3808
|
+
return {
|
|
3809
|
+
id: incident.id,
|
|
3810
|
+
timestamp: incident.timestamp,
|
|
3811
|
+
status: incident.status,
|
|
3812
|
+
error: incident.error.message,
|
|
3813
|
+
symbols: incident.symbols,
|
|
3814
|
+
environment: incident.environment,
|
|
3815
|
+
matches: matches.map((m) => ({
|
|
3816
|
+
patternId: m.pattern.id,
|
|
3817
|
+
name: m.pattern.name,
|
|
3818
|
+
confidence: m.confidence,
|
|
3819
|
+
strategy: m.pattern.resolution.strategy,
|
|
3820
|
+
description: m.pattern.resolution.description
|
|
3821
|
+
}))
|
|
3822
|
+
};
|
|
3823
|
+
});
|
|
3824
|
+
return JSON.stringify({ count: results.length, incidents: results }, null, 2);
|
|
3825
|
+
}
|
|
3826
|
+
case "sentinel_show": {
|
|
3827
|
+
const { incidentId, includeTimeline, includeSimilar } = args;
|
|
3828
|
+
const incident = store.getIncident(incidentId);
|
|
3829
|
+
if (!incident) {
|
|
3830
|
+
return JSON.stringify({ error: `Incident ${incidentId} not found` });
|
|
3831
|
+
}
|
|
3832
|
+
const matches = matcher.match(incident, { maxResults: 5 });
|
|
3833
|
+
const result = {
|
|
3834
|
+
incident,
|
|
3835
|
+
matches: matches.map((m) => ({
|
|
3836
|
+
patternId: m.pattern.id,
|
|
3837
|
+
name: m.pattern.name,
|
|
3838
|
+
confidence: m.confidence,
|
|
3839
|
+
matchedCriteria: m.matchedCriteria,
|
|
3840
|
+
resolution: m.pattern.resolution
|
|
3841
|
+
}))
|
|
3842
|
+
};
|
|
3843
|
+
if (includeTimeline && incident.flowPosition) {
|
|
3844
|
+
const timeline = new TimelineBuilder().build(incident);
|
|
3845
|
+
if (timeline) {
|
|
3846
|
+
result.timeline = new TimelineBuilder().renderStructured(timeline);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
if (includeSimilar) {
|
|
3850
|
+
const similar = store.getRecentIncidents({ symbol: Object.values(incident.symbols)[0], limit: 5 }).filter((i) => i.id !== incidentId);
|
|
3851
|
+
result.similar = similar.map((i) => ({
|
|
3852
|
+
id: i.id,
|
|
3853
|
+
error: i.error.message,
|
|
3854
|
+
status: i.status
|
|
3855
|
+
}));
|
|
3856
|
+
}
|
|
3857
|
+
return JSON.stringify(result, null, 2);
|
|
3858
|
+
}
|
|
3859
|
+
case "sentinel_resolve": {
|
|
3860
|
+
const { incidentId, patternId, commitHash, prUrl, notes, wontFix } = args;
|
|
3861
|
+
const incident = store.getIncident(incidentId);
|
|
3862
|
+
if (!incident) {
|
|
3863
|
+
return JSON.stringify({ error: `Incident ${incidentId} not found` });
|
|
3864
|
+
}
|
|
3865
|
+
if (wontFix) {
|
|
3866
|
+
store.updateIncident(incidentId, {
|
|
3867
|
+
status: "wont-fix",
|
|
3868
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3869
|
+
resolvedBy: "manual",
|
|
3870
|
+
resolution: { notes }
|
|
3871
|
+
});
|
|
3872
|
+
return JSON.stringify({
|
|
3873
|
+
success: true,
|
|
3874
|
+
message: `Incident ${incidentId} marked as won't fix`
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
store.recordResolution({ incidentId, patternId, commitHash, prUrl, notes });
|
|
3878
|
+
return JSON.stringify({
|
|
3879
|
+
success: true,
|
|
3880
|
+
message: `Incident ${incidentId} resolved`,
|
|
3881
|
+
patternId,
|
|
3882
|
+
commitHash,
|
|
3883
|
+
prUrl
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3886
|
+
case "sentinel_patterns": {
|
|
3887
|
+
const { symbol, minConfidence, source } = args;
|
|
3888
|
+
const patterns = store.getAllPatterns({
|
|
3889
|
+
source,
|
|
3890
|
+
minConfidence,
|
|
3891
|
+
includePrivate: false
|
|
3892
|
+
});
|
|
3893
|
+
let filtered = patterns;
|
|
3894
|
+
if (symbol) {
|
|
3895
|
+
filtered = patterns.filter((p) => {
|
|
3896
|
+
const symbols = p.pattern.symbols;
|
|
3897
|
+
return Object.values(symbols).some((v) => {
|
|
3898
|
+
if (!v) return false;
|
|
3899
|
+
if (Array.isArray(v)) return v.includes(symbol) || v.some((s) => symbol.match(s.replace("*", ".*")));
|
|
3900
|
+
return v === symbol || symbol.match(v.replace("*", ".*"));
|
|
3901
|
+
});
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
return JSON.stringify(
|
|
3905
|
+
{
|
|
3906
|
+
count: filtered.length,
|
|
3907
|
+
patterns: filtered.map((p) => ({
|
|
3908
|
+
id: p.id,
|
|
3909
|
+
name: p.name,
|
|
3910
|
+
description: p.description,
|
|
3911
|
+
confidence: p.confidence.score,
|
|
3912
|
+
resolution: p.resolution,
|
|
3913
|
+
tags: p.tags
|
|
3914
|
+
}))
|
|
3915
|
+
},
|
|
3916
|
+
null,
|
|
3917
|
+
2
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
case "sentinel_add_pattern": {
|
|
3921
|
+
const { id, name: name2, description, pattern, resolution, tags } = args;
|
|
3922
|
+
const input = {
|
|
3923
|
+
id,
|
|
3924
|
+
name: name2,
|
|
3925
|
+
description: description || "",
|
|
3926
|
+
pattern: {
|
|
3927
|
+
symbols: pattern.symbols || {},
|
|
3928
|
+
errorContains: pattern.errorContains,
|
|
3929
|
+
missingSignals: pattern.missingSignals
|
|
3930
|
+
},
|
|
3931
|
+
resolution: {
|
|
3932
|
+
description: resolution.description,
|
|
3933
|
+
strategy: resolution.strategy,
|
|
3934
|
+
priority: resolution.priority || "medium",
|
|
3935
|
+
codeHint: resolution.codeHint
|
|
3936
|
+
},
|
|
3937
|
+
source: "manual",
|
|
3938
|
+
private: false,
|
|
3939
|
+
tags: tags || []
|
|
3940
|
+
};
|
|
3941
|
+
store.addPattern(input);
|
|
3942
|
+
return JSON.stringify({
|
|
3943
|
+
success: true,
|
|
3944
|
+
message: `Pattern ${id} created`,
|
|
3945
|
+
pattern: input
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
case "sentinel_record": {
|
|
3949
|
+
const { error, symbols, environment, service, version, flowPosition } = args;
|
|
3950
|
+
const incidentId = store.recordIncident({
|
|
3951
|
+
error,
|
|
3952
|
+
symbols,
|
|
3953
|
+
environment,
|
|
3954
|
+
service,
|
|
3955
|
+
version,
|
|
3956
|
+
flowPosition
|
|
3957
|
+
});
|
|
3958
|
+
const incident = store.getIncident(incidentId);
|
|
3959
|
+
const matches = incident ? matcher.match(incident, { maxResults: 3 }) : [];
|
|
3960
|
+
return JSON.stringify(
|
|
3961
|
+
{
|
|
3962
|
+
success: true,
|
|
3963
|
+
incidentId,
|
|
3964
|
+
matches: matches.map((m) => ({
|
|
3965
|
+
patternId: m.pattern.id,
|
|
3966
|
+
confidence: m.confidence,
|
|
3967
|
+
resolution: m.pattern.resolution.description
|
|
3968
|
+
}))
|
|
3969
|
+
},
|
|
3970
|
+
null,
|
|
3971
|
+
2
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
case "sentinel_stats": {
|
|
3975
|
+
const { period = "7d", symbol } = args;
|
|
3976
|
+
const calculator = new StatsCalculator(store);
|
|
3977
|
+
if (symbol) {
|
|
3978
|
+
const health = calculator.getSymbolHealth(symbol);
|
|
3979
|
+
return JSON.stringify({ symbol, health }, null, 2);
|
|
3980
|
+
}
|
|
3981
|
+
const match = period.match(/^(\d+)d$/);
|
|
3982
|
+
const periodDays = match ? parseInt(match[1], 10) : 7;
|
|
3983
|
+
const stats = calculator.getStats(periodDays);
|
|
3984
|
+
return JSON.stringify({ period: `${periodDays}d`, stats }, null, 2);
|
|
3985
|
+
}
|
|
3986
|
+
case "sentinel_suggest_pattern": {
|
|
3987
|
+
const { incidentId, minOccurrences } = args;
|
|
3988
|
+
const suggester = new PatternSuggester(store);
|
|
3989
|
+
if (incidentId) {
|
|
3990
|
+
const incident = store.getIncident(incidentId);
|
|
3991
|
+
if (!incident) {
|
|
3992
|
+
return JSON.stringify({ error: `Incident ${incidentId} not found` });
|
|
3993
|
+
}
|
|
3994
|
+
const suggestion = suggester.suggestFromIncident(incident);
|
|
3995
|
+
return JSON.stringify({ source: "incident", incidentId, suggestion }, null, 2);
|
|
3996
|
+
}
|
|
3997
|
+
const candidates = suggester.findPatternCandidates(minOccurrences || 3);
|
|
3998
|
+
return JSON.stringify(
|
|
3999
|
+
{
|
|
4000
|
+
source: "analysis",
|
|
4001
|
+
candidates: candidates.slice(0, 5).map((c) => ({
|
|
4002
|
+
occurrences: c.occurrenceCount,
|
|
4003
|
+
sampleIncidents: c.incidents.slice(0, 3).map((i) => i.id),
|
|
4004
|
+
suggestion: c.suggestedPattern
|
|
4005
|
+
}))
|
|
4006
|
+
},
|
|
4007
|
+
null,
|
|
4008
|
+
2
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
4011
|
+
// ─── Observability Tools ──────────────────────────────────────
|
|
4012
|
+
case "sentinel_logs": {
|
|
4013
|
+
const { level, symbol, service, search, since, sessionId, correlationId, limit = 50 } = args;
|
|
4014
|
+
const logs = store.queryLogs({
|
|
4015
|
+
level,
|
|
4016
|
+
symbol,
|
|
4017
|
+
service,
|
|
4018
|
+
search,
|
|
4019
|
+
since,
|
|
4020
|
+
sessionId,
|
|
4021
|
+
correlationId,
|
|
4022
|
+
limit
|
|
4023
|
+
});
|
|
4024
|
+
const total = store.getLogCount({ level, symbol, service, since });
|
|
4025
|
+
return JSON.stringify({
|
|
4026
|
+
count: logs.length,
|
|
4027
|
+
total,
|
|
4028
|
+
logs: logs.map((l) => ({
|
|
4029
|
+
timestamp: l.timestamp,
|
|
4030
|
+
level: l.level,
|
|
4031
|
+
symbol: l.symbol,
|
|
4032
|
+
service: l.service,
|
|
4033
|
+
message: l.message,
|
|
4034
|
+
data: l.data,
|
|
4035
|
+
sessionId: l.sessionId,
|
|
4036
|
+
correlationId: l.correlationId,
|
|
4037
|
+
durationMs: l.durationMs
|
|
4038
|
+
}))
|
|
4039
|
+
}, null, 2);
|
|
4040
|
+
}
|
|
4041
|
+
case "sentinel_services": {
|
|
4042
|
+
const services = store.getServices();
|
|
4043
|
+
return JSON.stringify({
|
|
4044
|
+
count: services.length,
|
|
4045
|
+
services: services.map((s) => ({
|
|
4046
|
+
name: s.name,
|
|
4047
|
+
version: s.version,
|
|
4048
|
+
environment: s.environment,
|
|
4049
|
+
lastSeen: s.lastSeenAt,
|
|
4050
|
+
startedAt: s.startedAt,
|
|
4051
|
+
pid: s.pid
|
|
4052
|
+
}))
|
|
4053
|
+
}, null, 2);
|
|
4054
|
+
}
|
|
4055
|
+
case "sentinel_app_state": {
|
|
4056
|
+
const { service: svc } = args;
|
|
4057
|
+
const states = svc ? store.getAppState(svc) : store.getAllAppStates();
|
|
4058
|
+
return JSON.stringify({
|
|
4059
|
+
states: states.map((s) => ({
|
|
4060
|
+
service: s.service,
|
|
4061
|
+
sessionId: s.sessionId,
|
|
4062
|
+
state: s.state,
|
|
4063
|
+
activeFlows: s.activeFlows,
|
|
4064
|
+
activeGates: s.activeGates,
|
|
4065
|
+
timestamp: s.timestamp
|
|
4066
|
+
}))
|
|
4067
|
+
}, null, 2);
|
|
4068
|
+
}
|
|
4069
|
+
case "sentinel_validate_symbol": {
|
|
4070
|
+
const { symbol: sym } = args;
|
|
4071
|
+
const logCount = store.getLogCount({ symbol: sym });
|
|
4072
|
+
return JSON.stringify({
|
|
4073
|
+
symbol: sym,
|
|
4074
|
+
usedInLogs: logCount > 0,
|
|
4075
|
+
logCount,
|
|
4076
|
+
tip: logCount === 0 ? "This symbol has not appeared in any logs. It may be a typo or unused." : `This symbol has been used in ${logCount} log entries.`
|
|
4077
|
+
}, null, 2);
|
|
4078
|
+
}
|
|
4079
|
+
case "sentinel_flow_activity": {
|
|
4080
|
+
const { flowId, service: flowSvc, since: flowSince } = args;
|
|
4081
|
+
const flowLogs = store.queryLogs({ symbol: flowId, service: flowSvc, since: flowSince, limit: 100 });
|
|
4082
|
+
const flowEvents = flowLogs.filter((l) => ["flow", "signal", "gate"].includes(l.symbolType)).map((l) => ({
|
|
4083
|
+
timestamp: l.timestamp,
|
|
4084
|
+
symbol: l.symbol,
|
|
4085
|
+
symbolType: l.symbolType,
|
|
4086
|
+
service: l.service,
|
|
4087
|
+
message: l.message,
|
|
4088
|
+
level: l.level
|
|
4089
|
+
}));
|
|
4090
|
+
return JSON.stringify({ count: flowEvents.length, events: flowEvents }, null, 2);
|
|
4091
|
+
}
|
|
4092
|
+
case "sentinel_metrics": {
|
|
4093
|
+
const { name: metricName, type: metricType, service: metricSvc, since: metricSince, aggregate, limit: metricLimit } = args;
|
|
4094
|
+
if (aggregate && metricName) {
|
|
4095
|
+
const agg = store.aggregateMetric(metricName, { service: metricSvc, since: metricSince });
|
|
4096
|
+
return JSON.stringify(agg, null, 2);
|
|
4097
|
+
}
|
|
4098
|
+
const metrics = store.queryMetrics({
|
|
4099
|
+
name: metricName,
|
|
4100
|
+
type: metricType,
|
|
4101
|
+
service: metricSvc,
|
|
4102
|
+
since: metricSince,
|
|
4103
|
+
limit: Math.min(metricLimit || 50, 100)
|
|
4104
|
+
});
|
|
4105
|
+
return JSON.stringify({
|
|
4106
|
+
count: metrics.length,
|
|
4107
|
+
metrics: metrics.map((m) => ({
|
|
4108
|
+
timestamp: m.timestamp,
|
|
4109
|
+
name: m.name,
|
|
4110
|
+
type: m.type,
|
|
4111
|
+
value: m.value,
|
|
4112
|
+
tags: m.tags,
|
|
4113
|
+
service: m.service
|
|
4114
|
+
}))
|
|
4115
|
+
}, null, 2);
|
|
4116
|
+
}
|
|
4117
|
+
case "sentinel_traces": {
|
|
4118
|
+
const { traceId: tid, service: traceSvc, symbol: traceSym, since: traceSince, limit: traceLimit } = args;
|
|
4119
|
+
if (tid) {
|
|
4120
|
+
const trace = store.getTrace(tid);
|
|
4121
|
+
if (!trace) return JSON.stringify({ error: "Trace not found" });
|
|
4122
|
+
return JSON.stringify(trace, null, 2);
|
|
4123
|
+
}
|
|
4124
|
+
const traces = store.queryTraces({
|
|
4125
|
+
service: traceSvc,
|
|
4126
|
+
symbol: traceSym,
|
|
4127
|
+
since: traceSince,
|
|
4128
|
+
limit: Math.min(traceLimit || 10, 20)
|
|
4129
|
+
});
|
|
4130
|
+
return JSON.stringify({
|
|
4131
|
+
count: traces.length,
|
|
4132
|
+
traces: traces.map((t) => ({
|
|
4133
|
+
traceId: t.traceId,
|
|
4134
|
+
services: t.services,
|
|
4135
|
+
spanCount: t.spans.length,
|
|
4136
|
+
totalDurationMs: t.totalDurationMs,
|
|
4137
|
+
startTime: t.startTime,
|
|
4138
|
+
endTime: t.endTime
|
|
4139
|
+
}))
|
|
4140
|
+
}, null, 2);
|
|
4141
|
+
}
|
|
4142
|
+
default:
|
|
4143
|
+
return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
async function main() {
|
|
4147
|
+
console.error("[sentinel-mcp] Starting...");
|
|
4148
|
+
const store = getStorage();
|
|
4149
|
+
await store.ensureReady();
|
|
4150
|
+
console.error("[sentinel-mcp] Storage initialized");
|
|
4151
|
+
const server = new Server(
|
|
4152
|
+
{ name: "sentinel", version: "0.2.0" },
|
|
4153
|
+
{ capabilities: { tools: {} } }
|
|
4154
|
+
);
|
|
4155
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
4156
|
+
tools: getToolsList()
|
|
4157
|
+
}));
|
|
4158
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4159
|
+
const { name, arguments: args } = request.params;
|
|
4160
|
+
try {
|
|
4161
|
+
const text = await handleTool(name, args || {});
|
|
4162
|
+
return { content: [{ type: "text", text }] };
|
|
4163
|
+
} catch (error) {
|
|
4164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4165
|
+
return {
|
|
4166
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
4167
|
+
isError: true
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
});
|
|
4171
|
+
server.onerror = (error) => {
|
|
4172
|
+
console.error("[sentinel-mcp] Server error:", error);
|
|
4173
|
+
};
|
|
4174
|
+
const transport = new StdioServerTransport();
|
|
4175
|
+
await server.connect(transport);
|
|
4176
|
+
console.error("[sentinel-mcp] Running on stdio");
|
|
4177
|
+
}
|
|
4178
|
+
main().catch((err) => {
|
|
4179
|
+
console.error("[sentinel-mcp] Fatal:", err);
|
|
4180
|
+
process.exit(1);
|
|
4181
|
+
});
|