@a-company/sentinel 0.2.0 → 3.5.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.
@@ -8,7 +8,7 @@ import initSqlJs from "sql.js";
8
8
  import { v4 as uuidv4 } from "uuid";
9
9
  import * as path from "path";
10
10
  import * as fs from "fs";
11
- var SCHEMA_VERSION = 2;
11
+ var SCHEMA_VERSION = 4;
12
12
  var DEFAULT_CONFIDENCE = {
13
13
  score: 50,
14
14
  timesMatched: 0,
@@ -128,6 +128,72 @@ var SentinelStorage = class {
128
128
  notes TEXT
129
129
  );
130
130
 
131
+ -- Structured logs table
132
+ CREATE TABLE IF NOT EXISTS logs (
133
+ id TEXT PRIMARY KEY,
134
+ timestamp TEXT NOT NULL,
135
+ level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
136
+ symbol TEXT NOT NULL,
137
+ symbol_type TEXT NOT NULL DEFAULT 'raw',
138
+ message TEXT NOT NULL,
139
+ data_json TEXT,
140
+ service TEXT NOT NULL,
141
+ session_id TEXT,
142
+ correlation_id TEXT,
143
+ duration_ms REAL,
144
+ environment TEXT
145
+ );
146
+
147
+ -- Service registry
148
+ CREATE TABLE IF NOT EXISTS services (
149
+ name TEXT PRIMARY KEY,
150
+ version TEXT,
151
+ pid INTEGER,
152
+ started_at TEXT NOT NULL,
153
+ last_seen_at TEXT NOT NULL,
154
+ environment TEXT,
155
+ metadata_json TEXT
156
+ );
157
+
158
+ -- Live app state snapshots (latest-wins per service+session)
159
+ CREATE TABLE IF NOT EXISTS app_state (
160
+ service TEXT NOT NULL,
161
+ session_id TEXT NOT NULL,
162
+ timestamp TEXT NOT NULL,
163
+ state_json TEXT NOT NULL,
164
+ active_flows_json TEXT,
165
+ active_gates_json TEXT,
166
+ PRIMARY KEY (service, session_id)
167
+ );
168
+
169
+ -- Metrics table
170
+ CREATE TABLE IF NOT EXISTS metrics (
171
+ id TEXT PRIMARY KEY,
172
+ timestamp TEXT NOT NULL,
173
+ name TEXT NOT NULL,
174
+ type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
175
+ value REAL NOT NULL,
176
+ tags_json TEXT DEFAULT '{}',
177
+ service TEXT NOT NULL,
178
+ environment TEXT
179
+ );
180
+
181
+ -- Traces table
182
+ CREATE TABLE IF NOT EXISTS traces (
183
+ trace_id TEXT NOT NULL,
184
+ span_id TEXT PRIMARY KEY,
185
+ parent_span_id TEXT,
186
+ service TEXT NOT NULL,
187
+ symbol TEXT NOT NULL,
188
+ operation TEXT NOT NULL,
189
+ start_time TEXT NOT NULL,
190
+ end_time TEXT,
191
+ duration_ms REAL,
192
+ status TEXT NOT NULL DEFAULT 'ok',
193
+ tags_json TEXT DEFAULT '{}',
194
+ log_ids_json TEXT DEFAULT '[]'
195
+ );
196
+
131
197
  -- Indexes
132
198
  CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
133
199
  CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
@@ -137,6 +203,18 @@ var SentinelStorage = class {
137
203
  CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
138
204
  CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
139
205
  CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
206
+ CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
207
+ CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
208
+ CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
209
+ CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
210
+ CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
211
+ CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
212
+ CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
213
+ CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
214
+ CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
215
+ CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
216
+ CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
217
+ CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
140
218
  `);
141
219
  this.db.run(
142
220
  "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
@@ -266,6 +344,101 @@ var SentinelStorage = class {
266
344
  this.db.run(
267
345
  "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
268
346
  );
347
+ currentVersion = 2;
348
+ }
349
+ if (currentVersion < 3) {
350
+ try {
351
+ this.db.run(`
352
+ CREATE TABLE IF NOT EXISTS logs (
353
+ id TEXT PRIMARY KEY,
354
+ timestamp TEXT NOT NULL,
355
+ level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
356
+ symbol TEXT NOT NULL,
357
+ symbol_type TEXT NOT NULL DEFAULT 'raw',
358
+ message TEXT NOT NULL,
359
+ data_json TEXT,
360
+ service TEXT NOT NULL,
361
+ session_id TEXT,
362
+ correlation_id TEXT,
363
+ duration_ms REAL,
364
+ environment TEXT
365
+ );
366
+
367
+ CREATE TABLE IF NOT EXISTS services (
368
+ name TEXT PRIMARY KEY,
369
+ version TEXT,
370
+ pid INTEGER,
371
+ started_at TEXT NOT NULL,
372
+ last_seen_at TEXT NOT NULL,
373
+ environment TEXT,
374
+ metadata_json TEXT
375
+ );
376
+
377
+ CREATE TABLE IF NOT EXISTS app_state (
378
+ service TEXT NOT NULL,
379
+ session_id TEXT NOT NULL,
380
+ timestamp TEXT NOT NULL,
381
+ state_json TEXT NOT NULL,
382
+ active_flows_json TEXT,
383
+ active_gates_json TEXT,
384
+ PRIMARY KEY (service, session_id)
385
+ );
386
+
387
+ CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
388
+ CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
389
+ CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
390
+ CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
391
+ CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
392
+ CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
393
+ `);
394
+ } catch {
395
+ }
396
+ this.db.run(
397
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '3')"
398
+ );
399
+ currentVersion = 3;
400
+ }
401
+ if (currentVersion < 4) {
402
+ try {
403
+ this.db.run(`
404
+ CREATE TABLE IF NOT EXISTS metrics (
405
+ id TEXT PRIMARY KEY,
406
+ timestamp TEXT NOT NULL,
407
+ name TEXT NOT NULL,
408
+ type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
409
+ value REAL NOT NULL,
410
+ tags_json TEXT DEFAULT '{}',
411
+ service TEXT NOT NULL,
412
+ environment TEXT
413
+ );
414
+
415
+ CREATE TABLE IF NOT EXISTS traces (
416
+ trace_id TEXT NOT NULL,
417
+ span_id TEXT PRIMARY KEY,
418
+ parent_span_id TEXT,
419
+ service TEXT NOT NULL,
420
+ symbol TEXT NOT NULL,
421
+ operation TEXT NOT NULL,
422
+ start_time TEXT NOT NULL,
423
+ end_time TEXT,
424
+ duration_ms REAL,
425
+ status TEXT NOT NULL DEFAULT 'ok',
426
+ tags_json TEXT DEFAULT '{}',
427
+ log_ids_json TEXT DEFAULT '[]'
428
+ );
429
+
430
+ CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
431
+ CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
432
+ CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
433
+ CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
434
+ CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
435
+ CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
436
+ `);
437
+ } catch {
438
+ }
439
+ this.db.run(
440
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '4')"
441
+ );
269
442
  }
270
443
  }
271
444
  /**
@@ -1240,175 +1413,789 @@ var SentinelStorage = class {
1240
1413
  notes: obj.notes || void 0
1241
1414
  };
1242
1415
  }
1243
- close() {
1244
- if (this.db) {
1245
- this.save();
1246
- this.db.close();
1247
- this.db = null;
1248
- }
1416
+ // ─── Structured Logs ─────────────────────────────────────────────
1417
+ inferSymbolType(symbol) {
1418
+ if (symbol.startsWith("#")) return "component";
1419
+ if (symbol.startsWith("^")) return "gate";
1420
+ if (symbol.startsWith("!")) return "signal";
1421
+ if (symbol.startsWith("$")) return "flow";
1422
+ if (symbol.startsWith("~")) return "aspect";
1423
+ return "raw";
1249
1424
  }
1250
- };
1251
-
1252
- // src/matcher.ts
1253
- var DEFAULT_CONFIG = {
1254
- minScore: 30,
1255
- maxResults: 5,
1256
- boostConfidence: true
1257
- };
1258
- var PatternMatcher = class {
1259
- constructor(storage) {
1260
- this.storage = storage;
1425
+ insertLog(input) {
1426
+ this.initializeSync();
1427
+ const id = input.id || uuidv4();
1428
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1429
+ const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
1430
+ this.db.run(
1431
+ `INSERT INTO logs (
1432
+ id, timestamp, level, symbol, symbol_type, message, data_json,
1433
+ service, session_id, correlation_id, duration_ms, environment
1434
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1435
+ [
1436
+ id,
1437
+ timestamp,
1438
+ input.level,
1439
+ input.symbol,
1440
+ symbolType,
1441
+ input.message,
1442
+ input.data ? JSON.stringify(input.data) : null,
1443
+ input.service,
1444
+ input.sessionId || null,
1445
+ input.correlationId || null,
1446
+ input.durationMs ?? null,
1447
+ input.environment || null
1448
+ ]
1449
+ );
1450
+ this.save();
1451
+ return id;
1261
1452
  }
1262
- /**
1263
- * Match an incident against all patterns and return ranked results
1264
- */
1265
- match(incident, config = {}) {
1266
- const { minScore, maxResults, boostConfidence } = {
1267
- ...DEFAULT_CONFIG,
1268
- ...config
1269
- };
1270
- const patterns = this.storage.getAllPatterns({ includePrivate: true });
1271
- const matches = [];
1272
- for (const pattern of patterns) {
1273
- if (!this.matchEnvironment(pattern, incident)) {
1274
- continue;
1275
- }
1276
- const { score, matchedCriteria } = this.scoreMatch(pattern, incident);
1277
- if (score >= minScore) {
1278
- let confidence = score;
1279
- if (boostConfidence) {
1280
- const confidenceFactor = pattern.confidence.score / 100;
1281
- confidence = score * (0.5 + 0.5 * confidenceFactor);
1282
- }
1283
- matches.push({
1284
- pattern,
1285
- score,
1286
- matchedCriteria,
1287
- confidence: Math.round(confidence)
1288
- });
1289
- this.storage.updatePatternConfidence(pattern.id, "matched");
1453
+ insertLogBatch(entries) {
1454
+ this.initializeSync();
1455
+ let accepted = 0;
1456
+ const errors = [];
1457
+ for (const input of entries) {
1458
+ try {
1459
+ const id = input.id || uuidv4();
1460
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1461
+ const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
1462
+ this.db.run(
1463
+ `INSERT INTO logs (
1464
+ id, timestamp, level, symbol, symbol_type, message, data_json,
1465
+ service, session_id, correlation_id, duration_ms, environment
1466
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1467
+ [
1468
+ id,
1469
+ timestamp,
1470
+ input.level,
1471
+ input.symbol,
1472
+ symbolType,
1473
+ input.message,
1474
+ input.data ? JSON.stringify(input.data) : null,
1475
+ input.service,
1476
+ input.sessionId || null,
1477
+ input.correlationId || null,
1478
+ input.durationMs ?? null,
1479
+ input.environment || null
1480
+ ]
1481
+ );
1482
+ accepted++;
1483
+ } catch (err) {
1484
+ errors.push(err instanceof Error ? err.message : String(err));
1290
1485
  }
1291
1486
  }
1292
- return matches.sort((a, b) => b.confidence - a.confidence).slice(0, maxResults);
1487
+ this.save();
1488
+ return { accepted, errors };
1293
1489
  }
1294
- /**
1295
- * Test a pattern against historical incidents
1296
- */
1297
- testPattern(pattern, limit = 100) {
1298
- const incidents = this.storage.getRecentIncidents({ limit });
1299
- const wouldMatch = [];
1300
- let totalScore = 0;
1301
- for (const incident of incidents) {
1302
- if (!this.matchEnvironment(pattern, incident)) {
1303
- continue;
1304
- }
1305
- const { score } = this.scoreMatch(pattern, incident);
1306
- if (score >= 30) {
1307
- wouldMatch.push(incident);
1308
- totalScore += score;
1309
- }
1490
+ queryLogs(options = {}) {
1491
+ this.initializeSync();
1492
+ const { limit = 100, offset = 0 } = options;
1493
+ const conditions = [];
1494
+ const params = [];
1495
+ if (options.level) {
1496
+ conditions.push("level = ?");
1497
+ params.push(options.level);
1310
1498
  }
1311
- return {
1312
- wouldMatch,
1313
- matchCount: wouldMatch.length,
1314
- avgScore: wouldMatch.length > 0 ? Math.round(totalScore / wouldMatch.length) : 0
1315
- };
1316
- }
1317
- /**
1318
- * Score how well a pattern matches an incident
1319
- */
1320
- scoreMatch(pattern, incident) {
1321
- let score = 0;
1322
- const matchedCriteria = {
1323
- symbols: [],
1324
- errorKeywords: [],
1325
- missingSignals: []
1326
- };
1327
- const symbolScore = this.matchSymbols(
1328
- pattern.pattern.symbols,
1329
- incident.symbols,
1330
- matchedCriteria.symbols
1331
- );
1332
- score += Math.min(symbolScore, 50);
1333
- const errorScore = this.matchErrorText(
1334
- pattern,
1335
- incident,
1336
- matchedCriteria.errorKeywords
1499
+ if (options.symbol) {
1500
+ conditions.push("symbol LIKE ?");
1501
+ params.push(`%${options.symbol}%`);
1502
+ }
1503
+ if (options.service) {
1504
+ conditions.push("service = ?");
1505
+ params.push(options.service);
1506
+ }
1507
+ if (options.sessionId) {
1508
+ conditions.push("session_id = ?");
1509
+ params.push(options.sessionId);
1510
+ }
1511
+ if (options.correlationId) {
1512
+ conditions.push("correlation_id = ?");
1513
+ params.push(options.correlationId);
1514
+ }
1515
+ if (options.search) {
1516
+ conditions.push("message LIKE ?");
1517
+ params.push(`%${options.search}%`);
1518
+ }
1519
+ if (options.since) {
1520
+ conditions.push("timestamp >= ?");
1521
+ params.push(options.since);
1522
+ }
1523
+ if (options.until) {
1524
+ conditions.push("timestamp <= ?");
1525
+ params.push(options.until);
1526
+ }
1527
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1528
+ const result = this.db.exec(
1529
+ `SELECT * FROM logs ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1530
+ [...params, limit, offset]
1337
1531
  );
1338
- score += Math.min(errorScore, 25);
1339
- const signalScore = this.matchMissingSignals(
1340
- pattern,
1341
- incident,
1342
- matchedCriteria.missingSignals
1532
+ if (result.length === 0) return [];
1533
+ return result[0].values.map(
1534
+ (row) => this.rowToLogEntry(result[0].columns, row)
1343
1535
  );
1344
- score += Math.min(signalScore, 25);
1345
- score = Math.min(score, 100);
1346
- return { score, matchedCriteria };
1347
1536
  }
1348
- /**
1349
- * Match symbols between pattern and incident
1350
- */
1351
- matchSymbols(patternSymbols, incidentSymbols, matched) {
1352
- let score = 0;
1353
- const symbolTypes = [
1354
- "feature",
1355
- "component",
1356
- "flow",
1357
- "gate",
1358
- "signal",
1359
- "state",
1360
- "integration"
1361
- ];
1362
- for (const type of symbolTypes) {
1363
- const patternValue = patternSymbols[type];
1364
- const incidentValue = incidentSymbols[type];
1365
- if (!patternValue || !incidentValue) {
1366
- continue;
1367
- }
1368
- if (typeof patternValue === "string") {
1369
- if (this.matchSingleSymbol(patternValue, incidentValue)) {
1370
- score += patternValue.includes("*") ? 5 : 10;
1371
- matched.push(type);
1372
- }
1373
- } else if (Array.isArray(patternValue)) {
1374
- for (const pv of patternValue) {
1375
- if (this.matchSingleSymbol(pv, incidentValue)) {
1376
- score += 7;
1377
- matched.push(type);
1378
- break;
1379
- }
1380
- }
1381
- }
1537
+ getLogCount(options = {}) {
1538
+ this.initializeSync();
1539
+ const conditions = [];
1540
+ const params = [];
1541
+ if (options.level) {
1542
+ conditions.push("level = ?");
1543
+ params.push(options.level);
1382
1544
  }
1383
- return score;
1384
- }
1385
- /**
1386
- * Match a single symbol value (supports wildcards)
1387
- */
1388
- matchSingleSymbol(pattern, value) {
1389
- if (pattern === "*") {
1390
- return true;
1545
+ if (options.symbol) {
1546
+ conditions.push("symbol LIKE ?");
1547
+ params.push(`%${options.symbol}%`);
1391
1548
  }
1392
- if (pattern.endsWith("*")) {
1393
- const prefix = pattern.slice(0, -1);
1394
- return value.startsWith(prefix);
1549
+ if (options.service) {
1550
+ conditions.push("service = ?");
1551
+ params.push(options.service);
1395
1552
  }
1396
- if (pattern.startsWith("*")) {
1397
- const suffix = pattern.slice(1);
1398
- return value.endsWith(suffix);
1553
+ if (options.since) {
1554
+ conditions.push("timestamp >= ?");
1555
+ params.push(options.since);
1399
1556
  }
1400
- if (pattern.includes("*")) {
1401
- const regex = new RegExp(
1402
- "^" + pattern.replace(/\*/g, ".*") + "$"
1403
- );
1404
- return regex.test(value);
1557
+ if (options.until) {
1558
+ conditions.push("timestamp <= ?");
1559
+ params.push(options.until);
1405
1560
  }
1406
- return pattern === value;
1561
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1562
+ const result = this.db.exec(
1563
+ `SELECT COUNT(*) as count FROM logs ${whereClause}`,
1564
+ params
1565
+ );
1566
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1567
+ return result[0].values[0][0];
1407
1568
  }
1408
- /**
1409
- * Match error text keywords and regex
1410
- */
1411
- matchErrorText(pattern, incident, matched) {
1569
+ pruneLogs(maxCount) {
1570
+ this.initializeSync();
1571
+ if (maxCount <= 0) return 0;
1572
+ const currentCount = this.getLogCount();
1573
+ if (currentCount <= maxCount) return 0;
1574
+ const deleteCount = currentCount - maxCount;
1575
+ this.db.run(
1576
+ `DELETE FROM logs WHERE id IN (
1577
+ SELECT id FROM logs ORDER BY timestamp ASC LIMIT ?
1578
+ )`,
1579
+ [deleteCount]
1580
+ );
1581
+ this.save();
1582
+ return deleteCount;
1583
+ }
1584
+ rowToLogEntry(columns, row) {
1585
+ const obj = {};
1586
+ columns.forEach((col, i) => {
1587
+ obj[col] = row[i];
1588
+ });
1589
+ return {
1590
+ id: obj.id,
1591
+ timestamp: obj.timestamp,
1592
+ level: obj.level,
1593
+ symbol: obj.symbol,
1594
+ symbolType: obj.symbol_type || "raw",
1595
+ message: obj.message,
1596
+ data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
1597
+ service: obj.service,
1598
+ sessionId: obj.session_id || void 0,
1599
+ correlationId: obj.correlation_id || void 0,
1600
+ durationMs: obj.duration_ms || void 0,
1601
+ environment: obj.environment || void 0
1602
+ };
1603
+ }
1604
+ // ─── Service Registry ──────────────────────────────────────────
1605
+ registerService(reg) {
1606
+ this.initializeSync();
1607
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1608
+ this.db.run(
1609
+ `INSERT INTO services (name, version, pid, started_at, last_seen_at, environment, metadata_json)
1610
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1611
+ ON CONFLICT(name) DO UPDATE SET
1612
+ version = excluded.version,
1613
+ pid = excluded.pid,
1614
+ last_seen_at = excluded.last_seen_at,
1615
+ environment = excluded.environment,
1616
+ metadata_json = excluded.metadata_json`,
1617
+ [
1618
+ reg.name,
1619
+ reg.version || null,
1620
+ reg.pid ?? null,
1621
+ now,
1622
+ now,
1623
+ reg.environment || null,
1624
+ reg.metadata ? JSON.stringify(reg.metadata) : null
1625
+ ]
1626
+ );
1627
+ this.save();
1628
+ }
1629
+ updateServiceLastSeen(name) {
1630
+ this.initializeSync();
1631
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1632
+ this.db.run(
1633
+ "UPDATE services SET last_seen_at = ? WHERE name = ?",
1634
+ [now, name]
1635
+ );
1636
+ this.save();
1637
+ }
1638
+ getServices() {
1639
+ this.initializeSync();
1640
+ const result = this.db.exec(
1641
+ "SELECT * FROM services ORDER BY last_seen_at DESC"
1642
+ );
1643
+ if (result.length === 0) return [];
1644
+ return result[0].values.map((row) => {
1645
+ const obj = {};
1646
+ result[0].columns.forEach((col, i) => {
1647
+ obj[col] = row[i];
1648
+ });
1649
+ return {
1650
+ name: obj.name,
1651
+ version: obj.version || void 0,
1652
+ pid: obj.pid || void 0,
1653
+ startedAt: obj.started_at,
1654
+ lastSeenAt: obj.last_seen_at,
1655
+ environment: obj.environment || void 0,
1656
+ metadata: obj.metadata_json ? JSON.parse(obj.metadata_json) : void 0
1657
+ };
1658
+ });
1659
+ }
1660
+ // ─── App State ──────────────────────────────────────────────────
1661
+ upsertAppState(state) {
1662
+ this.initializeSync();
1663
+ this.db.run(
1664
+ `INSERT INTO app_state (service, session_id, timestamp, state_json, active_flows_json, active_gates_json)
1665
+ VALUES (?, ?, ?, ?, ?, ?)
1666
+ ON CONFLICT(service, session_id) DO UPDATE SET
1667
+ timestamp = excluded.timestamp,
1668
+ state_json = excluded.state_json,
1669
+ active_flows_json = excluded.active_flows_json,
1670
+ active_gates_json = excluded.active_gates_json`,
1671
+ [
1672
+ state.service,
1673
+ state.sessionId,
1674
+ state.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1675
+ JSON.stringify(state.state),
1676
+ state.activeFlows ? JSON.stringify(state.activeFlows) : null,
1677
+ state.activeGates ? JSON.stringify(state.activeGates) : null
1678
+ ]
1679
+ );
1680
+ this.save();
1681
+ }
1682
+ getAppState(service, sessionId) {
1683
+ this.initializeSync();
1684
+ let query = "SELECT * FROM app_state WHERE service = ?";
1685
+ const params = [service];
1686
+ if (sessionId) {
1687
+ query += " AND session_id = ?";
1688
+ params.push(sessionId);
1689
+ }
1690
+ query += " ORDER BY timestamp DESC";
1691
+ const result = this.db.exec(query, params);
1692
+ if (result.length === 0) return [];
1693
+ return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
1694
+ }
1695
+ getAllAppStates() {
1696
+ this.initializeSync();
1697
+ const result = this.db.exec(
1698
+ "SELECT * FROM app_state ORDER BY timestamp DESC"
1699
+ );
1700
+ if (result.length === 0) return [];
1701
+ return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
1702
+ }
1703
+ rowToAppState(columns, row) {
1704
+ const obj = {};
1705
+ columns.forEach((col, i) => {
1706
+ obj[col] = row[i];
1707
+ });
1708
+ return {
1709
+ service: obj.service,
1710
+ sessionId: obj.session_id,
1711
+ timestamp: obj.timestamp,
1712
+ state: JSON.parse(obj.state_json),
1713
+ activeFlows: obj.active_flows_json ? JSON.parse(obj.active_flows_json) : void 0,
1714
+ activeGates: obj.active_gates_json ? JSON.parse(obj.active_gates_json) : void 0
1715
+ };
1716
+ }
1717
+ // ─── Metrics ───────────────────────────────────────────────────
1718
+ insertMetric(input) {
1719
+ this.initializeSync();
1720
+ const id = uuidv4();
1721
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1722
+ this.db.run(
1723
+ `INSERT INTO metrics (
1724
+ id, timestamp, name, type, value, tags_json, service, environment
1725
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1726
+ [
1727
+ id,
1728
+ timestamp,
1729
+ input.name,
1730
+ input.type,
1731
+ input.value,
1732
+ JSON.stringify(input.tags || {}),
1733
+ input.service,
1734
+ input.environment || null
1735
+ ]
1736
+ );
1737
+ this.save();
1738
+ return id;
1739
+ }
1740
+ insertMetricBatch(entries) {
1741
+ this.initializeSync();
1742
+ let accepted = 0;
1743
+ const errors = [];
1744
+ for (const input of entries) {
1745
+ try {
1746
+ const id = uuidv4();
1747
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1748
+ this.db.run(
1749
+ `INSERT INTO metrics (
1750
+ id, timestamp, name, type, value, tags_json, service, environment
1751
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1752
+ [
1753
+ id,
1754
+ timestamp,
1755
+ input.name,
1756
+ input.type,
1757
+ input.value,
1758
+ JSON.stringify(input.tags || {}),
1759
+ input.service,
1760
+ input.environment || null
1761
+ ]
1762
+ );
1763
+ accepted++;
1764
+ } catch (err) {
1765
+ errors.push(err instanceof Error ? err.message : String(err));
1766
+ }
1767
+ }
1768
+ this.save();
1769
+ return { accepted, errors };
1770
+ }
1771
+ queryMetrics(options = {}) {
1772
+ this.initializeSync();
1773
+ const { limit = 100, offset = 0 } = options;
1774
+ const conditions = [];
1775
+ const params = [];
1776
+ if (options.name) {
1777
+ conditions.push("name = ?");
1778
+ params.push(options.name);
1779
+ }
1780
+ if (options.type) {
1781
+ conditions.push("type = ?");
1782
+ params.push(options.type);
1783
+ }
1784
+ if (options.service) {
1785
+ conditions.push("service = ?");
1786
+ params.push(options.service);
1787
+ }
1788
+ if (options.tag) {
1789
+ const eqIdx = options.tag.indexOf("=");
1790
+ if (eqIdx > 0) {
1791
+ const tagKey = options.tag.substring(0, eqIdx);
1792
+ const tagValue = options.tag.substring(eqIdx + 1);
1793
+ conditions.push("tags_json LIKE ?");
1794
+ params.push(`%"${tagKey}":"${tagValue}"%`);
1795
+ }
1796
+ }
1797
+ if (options.since) {
1798
+ conditions.push("timestamp >= ?");
1799
+ params.push(options.since);
1800
+ }
1801
+ if (options.until) {
1802
+ conditions.push("timestamp <= ?");
1803
+ params.push(options.until);
1804
+ }
1805
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1806
+ const result = this.db.exec(
1807
+ `SELECT * FROM metrics ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1808
+ [...params, limit, offset]
1809
+ );
1810
+ if (result.length === 0) return [];
1811
+ return result[0].values.map(
1812
+ (row) => this.rowToMetricEntry(result[0].columns, row)
1813
+ );
1814
+ }
1815
+ getMetricCount(options = {}) {
1816
+ this.initializeSync();
1817
+ const conditions = [];
1818
+ const params = [];
1819
+ if (options.name) {
1820
+ conditions.push("name = ?");
1821
+ params.push(options.name);
1822
+ }
1823
+ if (options.type) {
1824
+ conditions.push("type = ?");
1825
+ params.push(options.type);
1826
+ }
1827
+ if (options.service) {
1828
+ conditions.push("service = ?");
1829
+ params.push(options.service);
1830
+ }
1831
+ if (options.tag) {
1832
+ const eqIdx = options.tag.indexOf("=");
1833
+ if (eqIdx > 0) {
1834
+ const tagKey = options.tag.substring(0, eqIdx);
1835
+ const tagValue = options.tag.substring(eqIdx + 1);
1836
+ conditions.push("tags_json LIKE ?");
1837
+ params.push(`%"${tagKey}":"${tagValue}"%`);
1838
+ }
1839
+ }
1840
+ if (options.since) {
1841
+ conditions.push("timestamp >= ?");
1842
+ params.push(options.since);
1843
+ }
1844
+ if (options.until) {
1845
+ conditions.push("timestamp <= ?");
1846
+ params.push(options.until);
1847
+ }
1848
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1849
+ const result = this.db.exec(
1850
+ `SELECT COUNT(*) as count FROM metrics ${whereClause}`,
1851
+ params
1852
+ );
1853
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1854
+ return result[0].values[0][0];
1855
+ }
1856
+ aggregateMetric(name, options) {
1857
+ this.initializeSync();
1858
+ const conditions = ["name = ?"];
1859
+ const params = [name];
1860
+ if (options?.service) {
1861
+ conditions.push("service = ?");
1862
+ params.push(options.service);
1863
+ }
1864
+ if (options?.since) {
1865
+ conditions.push("timestamp >= ?");
1866
+ params.push(options.since);
1867
+ }
1868
+ if (options?.until) {
1869
+ conditions.push("timestamp <= ?");
1870
+ params.push(options.until);
1871
+ }
1872
+ const whereClause = `WHERE ${conditions.join(" AND ")}`;
1873
+ const result = this.db.exec(
1874
+ `SELECT COUNT(*) as count, SUM(value) as sum, MIN(value) as min, MAX(value) as max, AVG(value) as avg
1875
+ FROM metrics ${whereClause}`,
1876
+ params
1877
+ );
1878
+ if (result.length === 0 || result[0].values.length === 0) {
1879
+ return { name, count: 0, sum: 0, min: 0, max: 0, avg: 0 };
1880
+ }
1881
+ const row = result[0].values[0];
1882
+ return {
1883
+ name,
1884
+ count: row[0] || 0,
1885
+ sum: row[1] || 0,
1886
+ min: row[2] || 0,
1887
+ max: row[3] || 0,
1888
+ avg: row[4] || 0
1889
+ };
1890
+ }
1891
+ pruneMetrics(maxCount) {
1892
+ this.initializeSync();
1893
+ if (maxCount <= 0) return 0;
1894
+ const currentCount = this.getMetricCount();
1895
+ if (currentCount <= maxCount) return 0;
1896
+ const deleteCount = currentCount - maxCount;
1897
+ this.db.run(
1898
+ `DELETE FROM metrics WHERE id IN (
1899
+ SELECT id FROM metrics ORDER BY timestamp ASC LIMIT ?
1900
+ )`,
1901
+ [deleteCount]
1902
+ );
1903
+ this.save();
1904
+ return deleteCount;
1905
+ }
1906
+ rowToMetricEntry(columns, row) {
1907
+ const obj = {};
1908
+ columns.forEach((col, i) => {
1909
+ obj[col] = row[i];
1910
+ });
1911
+ return {
1912
+ id: obj.id,
1913
+ timestamp: obj.timestamp,
1914
+ name: obj.name,
1915
+ type: obj.type,
1916
+ value: obj.value,
1917
+ tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
1918
+ service: obj.service,
1919
+ environment: obj.environment || void 0
1920
+ };
1921
+ }
1922
+ // ─── Traces ───────────────────────────────────────────────────
1923
+ insertSpan(input) {
1924
+ this.initializeSync();
1925
+ const spanId = input.spanId || uuidv4();
1926
+ const startTime = input.startTime || (/* @__PURE__ */ new Date()).toISOString();
1927
+ this.db.run(
1928
+ `INSERT INTO traces (
1929
+ trace_id, span_id, parent_span_id, service, symbol, operation,
1930
+ start_time, end_time, duration_ms, status, tags_json, log_ids_json
1931
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1932
+ [
1933
+ input.traceId,
1934
+ spanId,
1935
+ input.parentSpanId || null,
1936
+ input.service,
1937
+ input.symbol,
1938
+ input.operation,
1939
+ startTime,
1940
+ input.endTime || null,
1941
+ input.durationMs ?? null,
1942
+ input.status || "ok",
1943
+ JSON.stringify(input.tags || {}),
1944
+ JSON.stringify(input.logIds || [])
1945
+ ]
1946
+ );
1947
+ this.save();
1948
+ return spanId;
1949
+ }
1950
+ getTrace(traceId) {
1951
+ this.initializeSync();
1952
+ const result = this.db.exec(
1953
+ "SELECT * FROM traces WHERE trace_id = ? ORDER BY start_time ASC",
1954
+ [traceId]
1955
+ );
1956
+ if (result.length === 0 || result[0].values.length === 0) return null;
1957
+ const spans = result[0].values.map(
1958
+ (row) => this.rowToTraceSpan(result[0].columns, row)
1959
+ );
1960
+ const services = [...new Set(spans.map((s) => s.service))];
1961
+ const startTimes = spans.map((s) => s.startTime);
1962
+ const endTimes = spans.filter((s) => s.endTime).map((s) => s.endTime);
1963
+ const startTime = startTimes.sort()[0];
1964
+ const endTime = endTimes.length > 0 ? endTimes.sort().reverse()[0] : startTime;
1965
+ const startMs = new Date(startTime).getTime();
1966
+ const endMs = new Date(endTime).getTime();
1967
+ const totalDurationMs = endMs - startMs;
1968
+ return {
1969
+ traceId,
1970
+ spans,
1971
+ services,
1972
+ totalDurationMs: totalDurationMs > 0 ? totalDurationMs : 0,
1973
+ startTime,
1974
+ endTime
1975
+ };
1976
+ }
1977
+ queryTraces(options = {}) {
1978
+ this.initializeSync();
1979
+ const conditions = [];
1980
+ const params = [];
1981
+ if (options.service) {
1982
+ conditions.push("service = ?");
1983
+ params.push(options.service);
1984
+ }
1985
+ if (options.symbol) {
1986
+ conditions.push("symbol = ?");
1987
+ params.push(options.symbol);
1988
+ }
1989
+ if (options.since) {
1990
+ conditions.push("start_time >= ?");
1991
+ params.push(options.since);
1992
+ }
1993
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1994
+ const traceLimit = Math.min(options.limit || 20, 20);
1995
+ const result = this.db.exec(
1996
+ `SELECT DISTINCT trace_id FROM traces ${whereClause} ORDER BY start_time DESC LIMIT ?`,
1997
+ [...params, traceLimit]
1998
+ );
1999
+ if (result.length === 0) return [];
2000
+ const traces = [];
2001
+ for (const row of result[0].values) {
2002
+ const traceId = row[0];
2003
+ const trace = this.getTrace(traceId);
2004
+ if (trace) {
2005
+ traces.push(trace);
2006
+ }
2007
+ }
2008
+ return traces;
2009
+ }
2010
+ rowToTraceSpan(columns, row) {
2011
+ const obj = {};
2012
+ columns.forEach((col, i) => {
2013
+ obj[col] = row[i];
2014
+ });
2015
+ return {
2016
+ traceId: obj.trace_id,
2017
+ spanId: obj.span_id,
2018
+ parentSpanId: obj.parent_span_id || void 0,
2019
+ service: obj.service,
2020
+ symbol: obj.symbol,
2021
+ operation: obj.operation,
2022
+ startTime: obj.start_time,
2023
+ endTime: obj.end_time || void 0,
2024
+ durationMs: obj.duration_ms || void 0,
2025
+ status: obj.status || "ok",
2026
+ tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
2027
+ logs: obj.log_ids_json ? JSON.parse(obj.log_ids_json) : []
2028
+ };
2029
+ }
2030
+ close() {
2031
+ if (this.db) {
2032
+ this.save();
2033
+ this.db.close();
2034
+ this.db = null;
2035
+ }
2036
+ }
2037
+ };
2038
+
2039
+ // src/matcher.ts
2040
+ var DEFAULT_CONFIG = {
2041
+ minScore: 30,
2042
+ maxResults: 5,
2043
+ boostConfidence: true
2044
+ };
2045
+ var PatternMatcher = class {
2046
+ constructor(storage) {
2047
+ this.storage = storage;
2048
+ }
2049
+ /**
2050
+ * Match an incident against all patterns and return ranked results
2051
+ */
2052
+ match(incident, config = {}) {
2053
+ const { minScore, maxResults, boostConfidence } = {
2054
+ ...DEFAULT_CONFIG,
2055
+ ...config
2056
+ };
2057
+ const patterns = this.storage.getAllPatterns({ includePrivate: true });
2058
+ const matches = [];
2059
+ for (const pattern of patterns) {
2060
+ if (!this.matchEnvironment(pattern, incident)) {
2061
+ continue;
2062
+ }
2063
+ const { score, matchedCriteria } = this.scoreMatch(pattern, incident);
2064
+ if (score >= minScore) {
2065
+ let confidence = score;
2066
+ if (boostConfidence) {
2067
+ const confidenceFactor = pattern.confidence.score / 100;
2068
+ confidence = score * (0.5 + 0.5 * confidenceFactor);
2069
+ }
2070
+ matches.push({
2071
+ pattern,
2072
+ score,
2073
+ matchedCriteria,
2074
+ confidence: Math.round(confidence)
2075
+ });
2076
+ this.storage.updatePatternConfidence(pattern.id, "matched");
2077
+ }
2078
+ }
2079
+ return matches.sort((a, b) => b.confidence - a.confidence).slice(0, maxResults);
2080
+ }
2081
+ /**
2082
+ * Test a pattern against historical incidents
2083
+ */
2084
+ testPattern(pattern, limit = 100) {
2085
+ const incidents = this.storage.getRecentIncidents({ limit });
2086
+ const wouldMatch = [];
2087
+ let totalScore = 0;
2088
+ for (const incident of incidents) {
2089
+ if (!this.matchEnvironment(pattern, incident)) {
2090
+ continue;
2091
+ }
2092
+ const { score } = this.scoreMatch(pattern, incident);
2093
+ if (score >= 30) {
2094
+ wouldMatch.push(incident);
2095
+ totalScore += score;
2096
+ }
2097
+ }
2098
+ return {
2099
+ wouldMatch,
2100
+ matchCount: wouldMatch.length,
2101
+ avgScore: wouldMatch.length > 0 ? Math.round(totalScore / wouldMatch.length) : 0
2102
+ };
2103
+ }
2104
+ /**
2105
+ * Score how well a pattern matches an incident
2106
+ */
2107
+ scoreMatch(pattern, incident) {
2108
+ let score = 0;
2109
+ const matchedCriteria = {
2110
+ symbols: [],
2111
+ errorKeywords: [],
2112
+ missingSignals: []
2113
+ };
2114
+ const symbolScore = this.matchSymbols(
2115
+ pattern.pattern.symbols,
2116
+ incident.symbols,
2117
+ matchedCriteria.symbols
2118
+ );
2119
+ score += Math.min(symbolScore, 50);
2120
+ const errorScore = this.matchErrorText(
2121
+ pattern,
2122
+ incident,
2123
+ matchedCriteria.errorKeywords
2124
+ );
2125
+ score += Math.min(errorScore, 25);
2126
+ const signalScore = this.matchMissingSignals(
2127
+ pattern,
2128
+ incident,
2129
+ matchedCriteria.missingSignals
2130
+ );
2131
+ score += Math.min(signalScore, 25);
2132
+ score = Math.min(score, 100);
2133
+ return { score, matchedCriteria };
2134
+ }
2135
+ /**
2136
+ * Match symbols between pattern and incident
2137
+ */
2138
+ matchSymbols(patternSymbols, incidentSymbols, matched) {
2139
+ let score = 0;
2140
+ const symbolTypes = [
2141
+ "feature",
2142
+ "component",
2143
+ "flow",
2144
+ "gate",
2145
+ "signal",
2146
+ "state",
2147
+ "integration"
2148
+ ];
2149
+ for (const type of symbolTypes) {
2150
+ const patternValue = patternSymbols[type];
2151
+ const incidentValue = incidentSymbols[type];
2152
+ if (!patternValue || !incidentValue) {
2153
+ continue;
2154
+ }
2155
+ if (typeof patternValue === "string") {
2156
+ if (this.matchSingleSymbol(patternValue, incidentValue)) {
2157
+ score += patternValue.includes("*") ? 5 : 10;
2158
+ matched.push(type);
2159
+ }
2160
+ } else if (Array.isArray(patternValue)) {
2161
+ for (const pv of patternValue) {
2162
+ if (this.matchSingleSymbol(pv, incidentValue)) {
2163
+ score += 7;
2164
+ matched.push(type);
2165
+ break;
2166
+ }
2167
+ }
2168
+ }
2169
+ }
2170
+ return score;
2171
+ }
2172
+ /**
2173
+ * Match a single symbol value (supports wildcards)
2174
+ */
2175
+ matchSingleSymbol(pattern, value) {
2176
+ if (pattern === "*") {
2177
+ return true;
2178
+ }
2179
+ if (pattern.endsWith("*")) {
2180
+ const prefix = pattern.slice(0, -1);
2181
+ return value.startsWith(prefix);
2182
+ }
2183
+ if (pattern.startsWith("*")) {
2184
+ const suffix = pattern.slice(1);
2185
+ return value.endsWith(suffix);
2186
+ }
2187
+ if (pattern.includes("*")) {
2188
+ const regex = new RegExp(
2189
+ "^" + pattern.replace(/\*/g, ".*") + "$"
2190
+ );
2191
+ return regex.test(value);
2192
+ }
2193
+ return pattern === value;
2194
+ }
2195
+ /**
2196
+ * Match error text keywords and regex
2197
+ */
2198
+ matchErrorText(pattern, incident, matched) {
1412
2199
  let score = 0;
1413
2200
  const errorMessage = incident.error.message.toLowerCase();
1414
2201
  const errorType = incident.error.type?.toLowerCase();
@@ -1902,10 +2689,12 @@ function loadAllSeedPatterns() {
1902
2689
 
1903
2690
  // src/server/index.ts
1904
2691
  import express from "express";
1905
- import * as path5 from "path";
1906
- import * as fs4 from "fs";
2692
+ import * as http from "http";
2693
+ import * as path6 from "path";
2694
+ import * as fs5 from "fs";
1907
2695
  import { fileURLToPath as fileURLToPath2 } from "url";
1908
2696
  import chalk3 from "chalk";
2697
+ import { WebSocketServer, WebSocket } from "ws";
1909
2698
 
1910
2699
  // src/server/routes/symbols.ts
1911
2700
  import { Router } from "express";
@@ -2002,7 +2791,7 @@ async function loadParadigmConfig(projectDir) {
2002
2791
  }
2003
2792
  async function loadWithPremiseCore(projectDir) {
2004
2793
  try {
2005
- const { aggregateFromDirectory } = await import("./dist-BPWLYV4U.js");
2794
+ const { aggregateFromDirectory } = await import("./dist-TYG2XME3.js");
2006
2795
  log.flow("load-symbols").info("Using premise-core aggregator", { path: projectDir });
2007
2796
  const result = await aggregateFromDirectory(projectDir);
2008
2797
  const counts = {};
@@ -2572,85 +3361,738 @@ function createIncidentsRouter(_projectDir) {
2572
3361
  res.status(404).json({ error: "Incident not found" });
2573
3362
  return;
2574
3363
  }
2575
- res.json({ incident });
2576
- } catch (error) {
2577
- console.error("Failed to load incident:", error);
2578
- res.status(500).json({ error: "Failed to load incident" });
2579
- }
2580
- });
2581
- router.post("/:id/resolve", async (req, res) => {
2582
- try {
2583
- const incident = storage.getIncident(req.params.id);
2584
- if (!incident) {
2585
- res.status(404).json({ error: "Incident not found" });
3364
+ res.json({ incident });
3365
+ } catch (error) {
3366
+ console.error("Failed to load incident:", error);
3367
+ res.status(500).json({ error: "Failed to load incident" });
3368
+ }
3369
+ });
3370
+ router.post("/:id/resolve", async (req, res) => {
3371
+ try {
3372
+ const incident = storage.getIncident(req.params.id);
3373
+ if (!incident) {
3374
+ res.status(404).json({ error: "Incident not found" });
3375
+ return;
3376
+ }
3377
+ storage.resolveIncident(req.params.id, {
3378
+ notes: req.body.notes,
3379
+ patternId: req.body.patternId
3380
+ });
3381
+ res.json({ success: true });
3382
+ } catch (error) {
3383
+ console.error("Failed to resolve incident:", error);
3384
+ res.status(500).json({ error: "Failed to resolve incident" });
3385
+ }
3386
+ });
3387
+ return router;
3388
+ }
3389
+
3390
+ // src/server/routes/patterns.ts
3391
+ import { Router as Router5 } from "express";
3392
+ function createPatternsRouter(_projectDir) {
3393
+ const router = Router5();
3394
+ const storage = new SentinelStorage();
3395
+ router.get("/", async (req, res) => {
3396
+ try {
3397
+ const source = req.query.source;
3398
+ const symbol = req.query.symbol;
3399
+ const minConfidence = parseInt(req.query.minConfidence) || void 0;
3400
+ const options = {};
3401
+ if (source && ["manual", "suggested", "imported", "community"].includes(source)) {
3402
+ options.source = source;
3403
+ }
3404
+ if (symbol) options.symbol = symbol;
3405
+ if (minConfidence) options.minConfidence = minConfidence;
3406
+ const patterns = storage.getAllPatterns(options);
3407
+ const summaries = patterns.map((pattern) => ({
3408
+ id: pattern.id,
3409
+ name: pattern.name,
3410
+ description: pattern.description,
3411
+ confidence: {
3412
+ score: pattern.confidence.score,
3413
+ timesMatched: pattern.confidence.timesMatched,
3414
+ timesResolved: pattern.confidence.timesResolved
3415
+ },
3416
+ tags: pattern.tags
3417
+ }));
3418
+ res.json({ patterns: summaries });
3419
+ } catch (error) {
3420
+ console.error("Failed to load patterns:", error);
3421
+ res.status(500).json({ error: "Failed to load patterns" });
3422
+ }
3423
+ });
3424
+ router.get("/:id", async (req, res) => {
3425
+ try {
3426
+ const pattern = storage.getPattern(req.params.id);
3427
+ if (!pattern) {
3428
+ res.status(404).json({ error: "Pattern not found" });
3429
+ return;
3430
+ }
3431
+ res.json({ pattern });
3432
+ } catch (error) {
3433
+ console.error("Failed to load pattern:", error);
3434
+ res.status(500).json({ error: "Failed to load pattern" });
3435
+ }
3436
+ });
3437
+ return router;
3438
+ }
3439
+
3440
+ // src/server/routes/logs.ts
3441
+ import { Router as Router6 } from "express";
3442
+ import { v4 as uuidv42 } from "uuid";
3443
+ function inferSymbolType(symbol) {
3444
+ if (symbol.startsWith("#")) return "component";
3445
+ if (symbol.startsWith("^")) return "gate";
3446
+ if (symbol.startsWith("!")) return "signal";
3447
+ if (symbol.startsWith("$")) return "flow";
3448
+ if (symbol.startsWith("~")) return "aspect";
3449
+ return "raw";
3450
+ }
3451
+ function validateSymbol(symbol, index) {
3452
+ const entry = index.find((e) => e.symbol === symbol);
3453
+ if (entry) return { known: true };
3454
+ const symbolName = symbol.replace(/^[#^!$~]/, "");
3455
+ let bestMatch;
3456
+ let bestScore = 0;
3457
+ for (const e of index) {
3458
+ const eName = e.symbol.replace(/^[#^!$~]/, "");
3459
+ let shared = 0;
3460
+ for (let i = 0; i < Math.min(symbolName.length, eName.length); i++) {
3461
+ if (symbolName[i] === eName[i]) shared++;
3462
+ else break;
3463
+ }
3464
+ const score = shared / Math.max(symbolName.length, eName.length);
3465
+ if (score > bestScore && score > 0.5) {
3466
+ bestScore = score;
3467
+ bestMatch = e.symbol;
3468
+ }
3469
+ }
3470
+ return { known: false, suggestion: bestMatch };
3471
+ }
3472
+ function autoPromoteToIncident(entry, storage) {
3473
+ try {
3474
+ const symbolType = inferSymbolType(entry.symbol);
3475
+ const symbols = {};
3476
+ if (symbolType === "component") symbols.component = entry.symbol;
3477
+ else if (symbolType === "gate") symbols.gate = entry.symbol;
3478
+ else if (symbolType === "signal") symbols.signal = entry.symbol;
3479
+ else if (symbolType === "flow") symbols.flow = entry.symbol;
3480
+ else symbols.component = entry.symbol;
3481
+ storage.recordIncident({
3482
+ error: {
3483
+ message: entry.message,
3484
+ type: "LogError"
3485
+ },
3486
+ symbols,
3487
+ environment: entry.environment || "unknown",
3488
+ service: entry.service
3489
+ });
3490
+ } catch {
3491
+ }
3492
+ }
3493
+ var insertsSincePrune = 0;
3494
+ function createLogsRouter(options) {
3495
+ const router = Router6();
3496
+ const { storage, serverConfig, onLogReceived, symbolIndex } = options;
3497
+ router.post("/", async (req, res) => {
3498
+ try {
3499
+ const body = req.body;
3500
+ let entries;
3501
+ if (Array.isArray(body.entries)) {
3502
+ entries = body.entries;
3503
+ } else if (body.level && body.symbol && body.message && body.service) {
3504
+ entries = [body];
3505
+ } else {
3506
+ res.status(400).json({ error: "Expected {entries: [...]} or a single log entry with level, symbol, message, service" });
3507
+ return;
3508
+ }
3509
+ if (entries.length > serverConfig.maxBatchSize) {
3510
+ res.status(413).json({
3511
+ error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
3512
+ });
3513
+ return;
3514
+ }
3515
+ for (let i = 0; i < entries.length; i++) {
3516
+ const e = entries[i];
3517
+ if (!e.level || !e.symbol || !e.message || !e.service) {
3518
+ res.status(400).json({
3519
+ error: `Entry ${i}: missing required fields (level, symbol, message, service)`
3520
+ });
3521
+ return;
3522
+ }
3523
+ if (!["debug", "info", "warn", "error"].includes(e.level)) {
3524
+ res.status(400).json({
3525
+ error: `Entry ${i}: invalid level "${e.level}", must be debug|info|warn|error`
3526
+ });
3527
+ return;
3528
+ }
3529
+ }
3530
+ const result = storage.insertLogBatch(entries);
3531
+ for (const input of entries) {
3532
+ const entry = {
3533
+ id: input.id || uuidv42(),
3534
+ timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
3535
+ level: input.level,
3536
+ symbol: input.symbol,
3537
+ symbolType: input.symbolType || inferSymbolType(input.symbol),
3538
+ message: input.message,
3539
+ data: input.data,
3540
+ service: input.service,
3541
+ sessionId: input.sessionId,
3542
+ correlationId: input.correlationId,
3543
+ durationMs: input.durationMs,
3544
+ environment: input.environment
3545
+ };
3546
+ let validation;
3547
+ if (symbolIndex) {
3548
+ validation = validateSymbol(entry.symbol, symbolIndex);
3549
+ }
3550
+ if (entry.level === "error") {
3551
+ autoPromoteToIncident(entry, storage);
3552
+ }
3553
+ if (onLogReceived) {
3554
+ onLogReceived(entry, validation);
3555
+ }
3556
+ }
3557
+ insertsSincePrune += result.accepted;
3558
+ if (serverConfig.maxLogs > 0 && insertsSincePrune >= serverConfig.pruneIntervalInserts) {
3559
+ insertsSincePrune = 0;
3560
+ storage.pruneLogs(serverConfig.maxLogs);
3561
+ }
3562
+ res.json({ accepted: result.accepted, errors: result.errors.length > 0 ? result.errors : void 0 });
3563
+ } catch (error) {
3564
+ res.status(500).json({ error: "Failed to insert logs" });
3565
+ }
3566
+ });
3567
+ router.get("/", async (req, res) => {
3568
+ try {
3569
+ const options2 = {
3570
+ level: req.query.level,
3571
+ symbol: req.query.symbol,
3572
+ service: req.query.service,
3573
+ sessionId: req.query.sessionId,
3574
+ correlationId: req.query.correlationId,
3575
+ search: req.query.search,
3576
+ since: req.query.since,
3577
+ until: req.query.until,
3578
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
3579
+ offset: req.query.offset ? parseInt(req.query.offset) : 0
3580
+ };
3581
+ const logs = storage.queryLogs(options2);
3582
+ const total = storage.getLogCount(options2);
3583
+ res.json({ count: logs.length, total, logs });
3584
+ } catch (error) {
3585
+ res.status(500).json({ error: "Failed to query logs" });
3586
+ }
3587
+ });
3588
+ return router;
3589
+ }
3590
+
3591
+ // src/server/routes/services.ts
3592
+ import { Router as Router7 } from "express";
3593
+ function createServicesRouter(options) {
3594
+ const router = Router7();
3595
+ const { storage } = options;
3596
+ router.post("/", async (req, res) => {
3597
+ try {
3598
+ const { name, version, pid, environment, metadata } = req.body;
3599
+ if (!name) {
3600
+ res.status(400).json({ error: "Missing required field: name" });
3601
+ return;
3602
+ }
3603
+ storage.registerService({ name, version, pid, environment, metadata });
3604
+ res.json({ success: true, service: name });
3605
+ } catch (error) {
3606
+ res.status(500).json({ error: "Failed to register service" });
3607
+ }
3608
+ });
3609
+ router.get("/", async (_req, res) => {
3610
+ try {
3611
+ const services = storage.getServices();
3612
+ res.json({ count: services.length, services });
3613
+ } catch (error) {
3614
+ res.status(500).json({ error: "Failed to list services" });
3615
+ }
3616
+ });
3617
+ return router;
3618
+ }
3619
+ function createStateRouter(options) {
3620
+ const router = Router7();
3621
+ const { storage } = options;
3622
+ router.post("/", async (req, res) => {
3623
+ try {
3624
+ const { service, sessionId, state, activeFlows, activeGates } = req.body;
3625
+ if (!service || !sessionId || !state) {
3626
+ res.status(400).json({ error: "Missing required fields: service, sessionId, state" });
3627
+ return;
3628
+ }
3629
+ storage.upsertAppState({
3630
+ service,
3631
+ sessionId,
3632
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3633
+ state,
3634
+ activeFlows,
3635
+ activeGates
3636
+ });
3637
+ storage.updateServiceLastSeen(service);
3638
+ res.json({ success: true });
3639
+ } catch (error) {
3640
+ res.status(500).json({ error: "Failed to update state" });
3641
+ }
3642
+ });
3643
+ router.get("/", async (_req, res) => {
3644
+ try {
3645
+ const states = storage.getAllAppStates();
3646
+ res.json({ count: states.length, states });
3647
+ } catch (error) {
3648
+ res.status(500).json({ error: "Failed to get states" });
3649
+ }
3650
+ });
3651
+ router.get("/:service", async (req, res) => {
3652
+ try {
3653
+ const states = storage.getAppState(req.params.service);
3654
+ res.json({ count: states.length, states });
3655
+ } catch (error) {
3656
+ res.status(500).json({ error: "Failed to get service state" });
3657
+ }
3658
+ });
3659
+ return router;
3660
+ }
3661
+
3662
+ // src/server/routes/metrics.ts
3663
+ import { Router as Router8 } from "express";
3664
+ var VALID_METRIC_TYPES = ["counter", "gauge", "histogram"];
3665
+ function createMetricsRouter(options) {
3666
+ const router = Router8();
3667
+ const { storage, serverConfig } = options;
3668
+ router.post("/", (req, res) => {
3669
+ try {
3670
+ const body = req.body;
3671
+ let entries;
3672
+ if (Array.isArray(body.entries)) {
3673
+ entries = body.entries;
3674
+ } else if (body.name && body.type && body.value !== void 0 && body.service) {
3675
+ entries = [body];
3676
+ } else {
3677
+ res.status(400).json({
3678
+ error: "Expected {entries: [...]} or a single metric with name, type, value, service"
3679
+ });
3680
+ return;
3681
+ }
3682
+ if (entries.length > serverConfig.maxBatchSize) {
3683
+ res.status(413).json({
3684
+ error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
3685
+ });
3686
+ return;
3687
+ }
3688
+ for (let i = 0; i < entries.length; i++) {
3689
+ const e = entries[i];
3690
+ if (!e.name || !e.type || e.value === void 0 || !e.service) {
3691
+ res.status(400).json({
3692
+ error: `Entry ${i}: missing required fields (name, type, value, service)`
3693
+ });
3694
+ return;
3695
+ }
3696
+ if (!VALID_METRIC_TYPES.includes(e.type)) {
3697
+ res.status(400).json({
3698
+ error: `Entry ${i}: invalid type "${e.type}", must be counter|gauge|histogram`
3699
+ });
3700
+ return;
3701
+ }
3702
+ }
3703
+ const result = storage.insertMetricBatch(entries);
3704
+ res.json({
3705
+ accepted: result.accepted,
3706
+ errors: result.errors.length > 0 ? result.errors : void 0
3707
+ });
3708
+ } catch {
3709
+ res.status(500).json({ error: "Failed to insert metrics" });
3710
+ }
3711
+ });
3712
+ router.get("/", (req, res) => {
3713
+ try {
3714
+ const options2 = {
3715
+ name: req.query.name,
3716
+ type: req.query.type,
3717
+ service: req.query.service,
3718
+ tag: req.query.tag,
3719
+ since: req.query.since,
3720
+ until: req.query.until,
3721
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
3722
+ offset: req.query.offset ? parseInt(req.query.offset) : 0
3723
+ };
3724
+ const metrics = storage.queryMetrics(options2);
3725
+ const total = storage.getMetricCount(options2);
3726
+ res.json({ count: metrics.length, total, metrics });
3727
+ } catch {
3728
+ res.status(500).json({ error: "Failed to query metrics" });
3729
+ }
3730
+ });
3731
+ router.get("/aggregate/:name", (req, res) => {
3732
+ try {
3733
+ const aggregation = storage.aggregateMetric(req.params.name, {
3734
+ service: req.query.service,
3735
+ since: req.query.since,
3736
+ until: req.query.until
3737
+ });
3738
+ res.json(aggregation);
3739
+ } catch {
3740
+ res.status(500).json({ error: "Failed to aggregate metric" });
3741
+ }
3742
+ });
3743
+ return router;
3744
+ }
3745
+
3746
+ // src/server/routes/traces.ts
3747
+ import { Router as Router9 } from "express";
3748
+ function createTracesRouter(options) {
3749
+ const router = Router9();
3750
+ const { storage } = options;
3751
+ router.post("/", (req, res) => {
3752
+ try {
3753
+ const body = req.body;
3754
+ if (!body.traceId || !body.service || !body.symbol || !body.operation) {
3755
+ res.status(400).json({
3756
+ error: "Missing required fields: traceId, service, symbol, operation"
3757
+ });
3758
+ return;
3759
+ }
3760
+ const spanId = storage.insertSpan(body);
3761
+ res.json({ spanId, traceId: body.traceId });
3762
+ } catch {
3763
+ res.status(500).json({ error: "Failed to insert trace span" });
3764
+ }
3765
+ });
3766
+ router.get("/", (req, res) => {
3767
+ try {
3768
+ const traces = storage.queryTraces({
3769
+ service: req.query.service,
3770
+ symbol: req.query.symbol,
3771
+ since: req.query.since,
3772
+ limit: req.query.limit ? parseInt(req.query.limit) : 20
3773
+ });
3774
+ res.json({ count: traces.length, traces });
3775
+ } catch {
3776
+ res.status(500).json({ error: "Failed to query traces" });
3777
+ }
3778
+ });
3779
+ router.get("/:traceId", (req, res) => {
3780
+ try {
3781
+ const trace = storage.getTrace(req.params.traceId);
3782
+ if (!trace) {
3783
+ res.status(404).json({ error: "Trace not found" });
3784
+ return;
3785
+ }
3786
+ res.json(trace);
3787
+ } catch {
3788
+ res.status(500).json({ error: "Failed to get trace" });
3789
+ }
3790
+ });
3791
+ return router;
3792
+ }
3793
+
3794
+ // src/server/middleware/auth.ts
3795
+ function createAuthMiddleware(config) {
3796
+ return function authMiddleware(requiredPermission) {
3797
+ return (req, res, next) => {
3798
+ if (!config.enabled) {
3799
+ next();
3800
+ return;
3801
+ }
3802
+ const authHeader = req.headers.authorization;
3803
+ if (!authHeader) {
3804
+ res.status(401).json({ error: "Authentication required. Provide Authorization: Bearer <token>" });
3805
+ return;
3806
+ }
3807
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
3808
+ if (!match) {
3809
+ res.status(401).json({ error: "Invalid authorization format. Use: Bearer <token>" });
3810
+ return;
3811
+ }
3812
+ const tokenValue = match[1];
3813
+ const tokenEntry = config.tokens.find((t) => t.token === tokenValue);
3814
+ if (!tokenEntry) {
3815
+ res.status(401).json({ error: "Invalid token" });
2586
3816
  return;
2587
3817
  }
2588
- storage.resolveIncident(req.params.id, {
2589
- notes: req.body.notes,
2590
- patternId: req.body.patternId
3818
+ if (tokenEntry.expiresAt && new Date(tokenEntry.expiresAt) < /* @__PURE__ */ new Date()) {
3819
+ res.status(401).json({ error: "Token expired" });
3820
+ return;
3821
+ }
3822
+ const permissionLevel = { read: 1, write: 2, admin: 3 };
3823
+ const hasPermission = tokenEntry.permissions.some(
3824
+ (p) => permissionLevel[p] >= permissionLevel[requiredPermission]
3825
+ );
3826
+ if (!hasPermission) {
3827
+ res.status(403).json({ error: `Insufficient permissions. Required: ${requiredPermission}` });
3828
+ return;
3829
+ }
3830
+ req.authToken = tokenEntry;
3831
+ next();
3832
+ };
3833
+ };
3834
+ }
3835
+
3836
+ // src/server/middleware/rate-limit.ts
3837
+ var serviceWindows = /* @__PURE__ */ new Map();
3838
+ var globalWindow = { count: 0, windowStart: Date.now() };
3839
+ var WINDOW_MS = 6e4;
3840
+ function getOrResetWindow(entry) {
3841
+ const now = Date.now();
3842
+ if (now - entry.windowStart > WINDOW_MS) {
3843
+ entry.count = 0;
3844
+ entry.windowStart = now;
3845
+ }
3846
+ return entry;
3847
+ }
3848
+ function createRateLimiter(config) {
3849
+ return (req, res, next) => {
3850
+ if (!config.enabled) {
3851
+ next();
3852
+ return;
3853
+ }
3854
+ const service = req.body?.service || req.body?.entries?.[0]?.service || req.query.service || "_unknown";
3855
+ const rule = config.perService[service] || config.global;
3856
+ if (rule.samplingRate < 1 && Math.random() > rule.samplingRate) {
3857
+ res.status(200).json({ accepted: 0, sampled: true, message: "Request dropped by sampling" });
3858
+ return;
3859
+ }
3860
+ const gw = getOrResetWindow(globalWindow);
3861
+ if (gw.count >= config.global.maxRequestsPerMinute) {
3862
+ res.status(429).json({
3863
+ error: "Global rate limit exceeded",
3864
+ retryAfterMs: WINDOW_MS - (Date.now() - gw.windowStart)
2591
3865
  });
2592
- res.json({ success: true });
2593
- } catch (error) {
2594
- console.error("Failed to resolve incident:", error);
2595
- res.status(500).json({ error: "Failed to resolve incident" });
3866
+ return;
2596
3867
  }
2597
- });
2598
- return router;
3868
+ if (!serviceWindows.has(service)) {
3869
+ serviceWindows.set(service, { count: 0, windowStart: Date.now() });
3870
+ }
3871
+ const sw = getOrResetWindow(serviceWindows.get(service));
3872
+ if (sw.count >= rule.maxRequestsPerMinute) {
3873
+ res.status(429).json({
3874
+ error: `Rate limit exceeded for service: ${service}`,
3875
+ retryAfterMs: WINDOW_MS - (Date.now() - sw.windowStart)
3876
+ });
3877
+ return;
3878
+ }
3879
+ const batchSize = req.body?.entries?.length || 1;
3880
+ if (batchSize > rule.maxEntriesPerBatch) {
3881
+ res.status(413).json({
3882
+ error: `Batch too large: ${batchSize} entries, max ${rule.maxEntriesPerBatch} for service ${service}`
3883
+ });
3884
+ return;
3885
+ }
3886
+ gw.count++;
3887
+ sw.count++;
3888
+ next();
3889
+ };
2599
3890
  }
2600
3891
 
2601
- // src/server/routes/patterns.ts
2602
- import { Router as Router5 } from "express";
2603
- function createPatternsRouter(_projectDir) {
2604
- const router = Router5();
2605
- const storage = new SentinelStorage();
2606
- router.get("/", async (req, res) => {
2607
- try {
2608
- const source = req.query.source;
2609
- const symbol = req.query.symbol;
2610
- const minConfidence = parseInt(req.query.minConfidence) || void 0;
2611
- const options = {};
2612
- if (source && ["manual", "suggested", "imported", "community"].includes(source)) {
2613
- options.source = source;
3892
+ // src/config.ts
3893
+ import * as fs4 from "fs";
3894
+ import * as path5 from "path";
3895
+
3896
+ // src/types.ts
3897
+ var DEFAULT_AUTH_CONFIG = {
3898
+ enabled: false,
3899
+ tokens: []
3900
+ };
3901
+ var DEFAULT_RATE_LIMIT_CONFIG = {
3902
+ enabled: false,
3903
+ global: {
3904
+ maxRequestsPerMinute: 600,
3905
+ maxEntriesPerBatch: 500,
3906
+ samplingRate: 1
3907
+ },
3908
+ perService: {}
3909
+ };
3910
+ var DEFAULT_SERVER_CONFIG = {
3911
+ port: 3838,
3912
+ maxLogs: 1e4,
3913
+ maxBatchSize: 500,
3914
+ wsMaxSubscribers: 256,
3915
+ pruneIntervalInserts: 100,
3916
+ logRetentionDays: 0,
3917
+ auth: DEFAULT_AUTH_CONFIG,
3918
+ rateLimit: DEFAULT_RATE_LIMIT_CONFIG
3919
+ };
3920
+
3921
+ // src/config.ts
3922
+ var CONFIG_FILES = [".sentinel.yaml", ".sentinel.yml"];
3923
+ function loadConfig(projectDir) {
3924
+ for (const filename of CONFIG_FILES) {
3925
+ const filePath = path5.join(projectDir, filename);
3926
+ if (fs4.existsSync(filePath)) {
3927
+ const content = fs4.readFileSync(filePath, "utf-8");
3928
+ return parseSimpleYaml(content);
3929
+ }
3930
+ }
3931
+ return null;
3932
+ }
3933
+ function writeConfig(projectDir, config) {
3934
+ const filePath = path5.join(projectDir, ".sentinel.yaml");
3935
+ const content = serializeSimpleYaml(config);
3936
+ fs4.writeFileSync(filePath, content, "utf-8");
3937
+ }
3938
+ function parseSimpleYaml(content) {
3939
+ const config = { version: "1.0", project: "" };
3940
+ const lines = content.split("\n");
3941
+ let currentSection = null;
3942
+ let currentSubSection = null;
3943
+ for (const line of lines) {
3944
+ const trimmed = line.trimEnd();
3945
+ if (!trimmed || trimmed.startsWith("#")) continue;
3946
+ const topMatch = trimmed.match(/^(\w+):\s*(.+)$/);
3947
+ if (topMatch) {
3948
+ const [, key, value] = topMatch;
3949
+ if (key === "version") config.version = value.replace(/['"]/g, "");
3950
+ else if (key === "project") config.project = value.replace(/['"]/g, "");
3951
+ else if (key === "environment") config.environment = value.replace(/['"]/g, "");
3952
+ currentSection = null;
3953
+ currentSubSection = null;
3954
+ continue;
3955
+ }
3956
+ const sectionMatch = trimmed.match(/^(\w+):$/);
3957
+ if (sectionMatch) {
3958
+ currentSection = sectionMatch[1];
3959
+ currentSubSection = null;
3960
+ if (currentSection === "symbols" && !config.symbols) {
3961
+ config.symbols = {};
2614
3962
  }
2615
- if (symbol) options.symbol = symbol;
2616
- if (minConfidence) options.minConfidence = minConfidence;
2617
- const patterns = storage.getAllPatterns(options);
2618
- const summaries = patterns.map((pattern) => ({
2619
- id: pattern.id,
2620
- name: pattern.name,
2621
- description: pattern.description,
2622
- confidence: {
2623
- score: pattern.confidence.score,
2624
- timesMatched: pattern.confidence.timesMatched,
2625
- timesResolved: pattern.confidence.timesResolved
2626
- },
2627
- tags: pattern.tags
2628
- }));
2629
- res.json({ patterns: summaries });
2630
- } catch (error) {
2631
- console.error("Failed to load patterns:", error);
2632
- res.status(500).json({ error: "Failed to load patterns" });
3963
+ if (currentSection === "routes" && !config.routes) {
3964
+ config.routes = {};
3965
+ }
3966
+ if (currentSection === "scrub" && !config.scrub) {
3967
+ config.scrub = {};
3968
+ }
3969
+ if (currentSection === "server" && !config.server) {
3970
+ config.server = {};
3971
+ }
3972
+ continue;
2633
3973
  }
2634
- });
2635
- router.get("/:id", async (req, res) => {
2636
- try {
2637
- const pattern = storage.getPattern(req.params.id);
2638
- if (!pattern) {
2639
- res.status(404).json({ error: "Pattern not found" });
2640
- return;
3974
+ const subMatch = trimmed.match(/^\s{2}(\w+):$/);
3975
+ if (subMatch && currentSection) {
3976
+ currentSubSection = subMatch[1];
3977
+ if (currentSection === "symbols" && config.symbols) {
3978
+ config.symbols[currentSubSection] = [];
2641
3979
  }
2642
- res.json({ pattern });
2643
- } catch (error) {
2644
- console.error("Failed to load pattern:", error);
2645
- res.status(500).json({ error: "Failed to load pattern" });
3980
+ if (currentSection === "scrub" && config.scrub) {
3981
+ config.scrub[currentSubSection] = [];
3982
+ }
3983
+ continue;
2646
3984
  }
2647
- });
2648
- return router;
3985
+ const listMatch = trimmed.match(/^\s+-\s+(.+)$/);
3986
+ if (listMatch && currentSection && currentSubSection) {
3987
+ const value = listMatch[1].replace(/['"]/g, "");
3988
+ if (currentSection === "symbols" && config.symbols) {
3989
+ const arr = config.symbols[currentSubSection];
3990
+ if (Array.isArray(arr)) arr.push(value);
3991
+ }
3992
+ if (currentSection === "scrub" && config.scrub) {
3993
+ const arr = config.scrub[currentSubSection];
3994
+ if (Array.isArray(arr)) arr.push(value);
3995
+ }
3996
+ continue;
3997
+ }
3998
+ const routeMatch = trimmed.match(/^\s+(['"]?\/[^'"]+['"]?):\s+['"]?([^'"]+)['"]?$/);
3999
+ if (routeMatch && currentSection === "routes" && config.routes) {
4000
+ const route = routeMatch[1].replace(/['"]/g, "");
4001
+ config.routes[route] = routeMatch[2];
4002
+ continue;
4003
+ }
4004
+ const serverKvMatch = trimmed.match(/^\s+(\w+):\s+(\d+)$/);
4005
+ if (serverKvMatch && currentSection === "server" && config.server) {
4006
+ const key = serverKvMatch[1];
4007
+ const value = parseInt(serverKvMatch[2], 10);
4008
+ if (key in { port: 1, maxLogs: 1, maxBatchSize: 1, wsMaxSubscribers: 1, pruneIntervalInserts: 1, logRetentionDays: 1 }) {
4009
+ config.server[key] = value;
4010
+ }
4011
+ continue;
4012
+ }
4013
+ }
4014
+ return config;
4015
+ }
4016
+ function serializeSimpleYaml(config) {
4017
+ const lines = [];
4018
+ lines.push(`# Sentinel Configuration`);
4019
+ lines.push(`# Auto-generated \u2014 edit freely`);
4020
+ lines.push("");
4021
+ lines.push(`version: "${config.version}"`);
4022
+ lines.push(`project: "${config.project}"`);
4023
+ if (config.environment) {
4024
+ lines.push(`environment: "${config.environment}"`);
4025
+ }
4026
+ if (config.symbols) {
4027
+ lines.push("");
4028
+ lines.push("symbols:");
4029
+ for (const [key, values] of Object.entries(config.symbols)) {
4030
+ if (values && values.length > 0) {
4031
+ lines.push(` ${key}:`);
4032
+ for (const v of values) {
4033
+ lines.push(` - ${v}`);
4034
+ }
4035
+ }
4036
+ }
4037
+ }
4038
+ if (config.routes && Object.keys(config.routes).length > 0) {
4039
+ lines.push("");
4040
+ lines.push("routes:");
4041
+ for (const [route, symbol] of Object.entries(config.routes)) {
4042
+ lines.push(` "${route}": ${symbol}`);
4043
+ }
4044
+ }
4045
+ if (config.scrub) {
4046
+ lines.push("");
4047
+ lines.push("scrub:");
4048
+ if (config.scrub.headers?.length) {
4049
+ lines.push(" headers:");
4050
+ for (const h of config.scrub.headers) {
4051
+ lines.push(` - ${h}`);
4052
+ }
4053
+ }
4054
+ if (config.scrub.fields?.length) {
4055
+ lines.push(" fields:");
4056
+ for (const f of config.scrub.fields) {
4057
+ lines.push(` - ${f}`);
4058
+ }
4059
+ }
4060
+ }
4061
+ if (config.server && Object.keys(config.server).length > 0) {
4062
+ lines.push("");
4063
+ lines.push("server:");
4064
+ for (const [key, value] of Object.entries(config.server)) {
4065
+ if (value !== void 0) {
4066
+ lines.push(` ${key}: ${value}`);
4067
+ }
4068
+ }
4069
+ }
4070
+ lines.push("");
4071
+ return lines.join("\n");
4072
+ }
4073
+ function loadServerConfig(projectDir) {
4074
+ const config = { ...DEFAULT_SERVER_CONFIG };
4075
+ const yamlConfig = projectDir ? loadConfig(projectDir) : null;
4076
+ const globalDir = path5.join(process.env.HOME || "~", ".paradigm");
4077
+ const globalConfig = loadConfig(globalDir);
4078
+ for (const src of [globalConfig, yamlConfig]) {
4079
+ if (src?.server) {
4080
+ if (src.server.port !== void 0) config.port = src.server.port;
4081
+ if (src.server.maxLogs !== void 0) config.maxLogs = src.server.maxLogs;
4082
+ if (src.server.maxBatchSize !== void 0) config.maxBatchSize = src.server.maxBatchSize;
4083
+ if (src.server.wsMaxSubscribers !== void 0) config.wsMaxSubscribers = src.server.wsMaxSubscribers;
4084
+ if (src.server.pruneIntervalInserts !== void 0) config.pruneIntervalInserts = src.server.pruneIntervalInserts;
4085
+ if (src.server.logRetentionDays !== void 0) config.logRetentionDays = src.server.logRetentionDays;
4086
+ }
4087
+ }
4088
+ if (process.env.SENTINEL_PORT) config.port = parseInt(process.env.SENTINEL_PORT, 10);
4089
+ if (process.env.SENTINEL_MAX_LOGS) config.maxLogs = parseInt(process.env.SENTINEL_MAX_LOGS, 10);
4090
+ return config;
2649
4091
  }
2650
4092
 
2651
4093
  // src/server/index.ts
2652
4094
  var __filename2 = fileURLToPath2(import.meta.url);
2653
- var __dirname2 = path5.dirname(__filename2);
4095
+ var __dirname2 = path6.dirname(__filename2);
2654
4096
  var log3 = {
2655
4097
  component(name) {
2656
4098
  const symbol = chalk3.magenta(`#${name}`);
@@ -2676,11 +4118,20 @@ var log3 = {
2676
4118
  };
2677
4119
  function createApp(options) {
2678
4120
  const app = express();
2679
- app.use(express.json());
4121
+ app.use(express.json({ limit: "5mb" }));
2680
4122
  app.use((_req, res, next) => {
2681
- res.header("Access-Control-Allow-Origin", "*");
4123
+ const corsOrigin = options.serverConfig?.cors?.origin;
4124
+ const origin = Array.isArray(corsOrigin) ? corsOrigin.join(", ") : corsOrigin ?? "*";
4125
+ res.header("Access-Control-Allow-Origin", origin);
2682
4126
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2683
- res.header("Access-Control-Allow-Headers", "Content-Type");
4127
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
4128
+ if (options.serverConfig?.cors?.credentials) {
4129
+ res.header("Access-Control-Allow-Credentials", "true");
4130
+ }
4131
+ if (_req.method === "OPTIONS") {
4132
+ res.sendStatus(204);
4133
+ return;
4134
+ }
2684
4135
  next();
2685
4136
  });
2686
4137
  app.use("/api/symbols", createSymbolsRouter(options.projectDir));
@@ -2688,27 +4139,135 @@ function createApp(options) {
2688
4139
  app.use("/api/commits", createCommitsRouter(options.projectDir));
2689
4140
  app.use("/api/incidents", createIncidentsRouter(options.projectDir));
2690
4141
  app.use("/api/patterns", createPatternsRouter(options.projectDir));
4142
+ if (options.storage && options.serverConfig) {
4143
+ const config = options.serverConfig;
4144
+ const auth = createAuthMiddleware(config.auth);
4145
+ const rateLimiter = createRateLimiter(config.rateLimit);
4146
+ app.use("/api/logs", rateLimiter, auth("write"), createLogsRouter({
4147
+ storage: options.storage,
4148
+ serverConfig: config,
4149
+ onLogReceived: options.onLogReceived,
4150
+ symbolIndex: options.symbolIndex
4151
+ }));
4152
+ app.use("/api/services", rateLimiter, auth("write"), createServicesRouter({ storage: options.storage }));
4153
+ app.use("/api/state", rateLimiter, auth("write"), createStateRouter({ storage: options.storage }));
4154
+ app.use("/api/metrics", rateLimiter, auth("write"), createMetricsRouter({
4155
+ storage: options.storage,
4156
+ serverConfig: config
4157
+ }));
4158
+ app.use("/api/traces", rateLimiter, auth("write"), createTracesRouter({
4159
+ storage: options.storage
4160
+ }));
4161
+ }
2691
4162
  app.get("/api/health", (_req, res) => {
2692
4163
  res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
2693
4164
  });
2694
- const uiDistPath = path5.join(__dirname2, "..", "..", "ui", "dist");
2695
- if (fs4.existsSync(uiDistPath)) {
4165
+ const uiDistPath = path6.join(__dirname2, "..", "..", "ui", "dist");
4166
+ if (fs5.existsSync(uiDistPath)) {
2696
4167
  app.use(express.static(uiDistPath));
2697
4168
  app.get("{*path}", (req, res) => {
2698
4169
  if (!req.path.startsWith("/api")) {
2699
- res.sendFile(path5.join(uiDistPath, "index.html"));
4170
+ res.sendFile(path6.join(uiDistPath, "index.html"));
2700
4171
  }
2701
4172
  });
2702
4173
  }
2703
4174
  return app;
2704
4175
  }
2705
4176
  async function startServer(options) {
2706
- const app = createApp(options);
4177
+ const serverConfig = loadServerConfig(options.projectDir);
4178
+ if (options.logPruneLimit !== void 0) {
4179
+ serverConfig.maxLogs = options.logPruneLimit;
4180
+ }
4181
+ const storage = new SentinelStorage(options.dbPath);
4182
+ await storage.ensureReady();
4183
+ let symbolIndex = [];
4184
+ try {
4185
+ symbolIndex = await loadSymbolIndex(options.projectDir);
4186
+ } catch {
4187
+ log3.component("sentinel-server").warn("Could not load symbol index for validation");
4188
+ }
4189
+ const wsClients = /* @__PURE__ */ new Set();
4190
+ function broadcast(message) {
4191
+ const data = JSON.stringify(message);
4192
+ for (const client of wsClients) {
4193
+ if (client.readyState === WebSocket.OPEN) {
4194
+ client.send(data);
4195
+ }
4196
+ }
4197
+ }
4198
+ function onLogReceived(entry, validation) {
4199
+ const message = { type: "log", entry };
4200
+ if (validation && !validation.known) {
4201
+ message.validation = validation;
4202
+ }
4203
+ broadcast(message);
4204
+ if (entry.symbolType === "signal" || entry.symbolType === "gate" || entry.symbolType === "flow") {
4205
+ broadcast({
4206
+ type: "flow_event",
4207
+ flowId: entry.symbolType === "flow" ? entry.symbol : void 0,
4208
+ nodeSymbol: entry.symbol,
4209
+ event: entry.symbolType,
4210
+ timestamp: entry.timestamp,
4211
+ service: entry.service
4212
+ });
4213
+ }
4214
+ }
4215
+ const app = createApp({
4216
+ ...options,
4217
+ storage,
4218
+ serverConfig,
4219
+ symbolIndex,
4220
+ onLogReceived
4221
+ });
2707
4222
  log3.component("sentinel-server").info("Starting server", { port: options.port });
2708
4223
  log3.component("sentinel-server").info("Project directory", { path: options.projectDir });
2709
4224
  return new Promise((resolve, reject) => {
2710
- const server = app.listen(options.port, () => {
4225
+ const httpServer = http.createServer(app);
4226
+ const wss = new WebSocketServer({ server: httpServer });
4227
+ wss.on("connection", (ws) => {
4228
+ if (wsClients.size >= serverConfig.wsMaxSubscribers) {
4229
+ ws.close(1013, "Max subscribers reached");
4230
+ return;
4231
+ }
4232
+ wsClients.add(ws);
4233
+ log3.component("sentinel-ws").info("Client connected", { total: wsClients.size });
4234
+ ws.on("message", (raw) => {
4235
+ try {
4236
+ const msg = JSON.parse(raw.toString());
4237
+ if (msg.method === "ping") {
4238
+ ws.send(JSON.stringify({
4239
+ jsonrpc: "2.0",
4240
+ result: { pong: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
4241
+ id: msg.id
4242
+ }));
4243
+ } else if (msg.method === "subscribe") {
4244
+ ws.send(JSON.stringify({
4245
+ jsonrpc: "2.0",
4246
+ result: { subscribed: true },
4247
+ id: msg.id
4248
+ }));
4249
+ } else if (msg.method === "query_logs") {
4250
+ const logs = storage.queryLogs(msg.params || {});
4251
+ ws.send(JSON.stringify({
4252
+ jsonrpc: "2.0",
4253
+ result: { logs },
4254
+ id: msg.id
4255
+ }));
4256
+ }
4257
+ } catch {
4258
+ }
4259
+ });
4260
+ ws.on("close", () => {
4261
+ wsClients.delete(ws);
4262
+ log3.component("sentinel-ws").info("Client disconnected", { total: wsClients.size });
4263
+ });
4264
+ ws.on("error", () => {
4265
+ wsClients.delete(ws);
4266
+ });
4267
+ });
4268
+ httpServer.listen(options.port, () => {
2711
4269
  log3.component("sentinel-server").success("Server running", { url: `http://localhost:${options.port}` });
4270
+ log3.component("sentinel-ws").success("WebSocket ready", { url: `ws://localhost:${options.port}` });
2712
4271
  if (options.open) {
2713
4272
  import("open").then((openModule) => {
2714
4273
  openModule.default(`http://localhost:${options.port}`);
@@ -2719,7 +4278,7 @@ async function startServer(options) {
2719
4278
  }
2720
4279
  resolve();
2721
4280
  });
2722
- server.on("error", (err) => {
4281
+ httpServer.on("error", (err) => {
2723
4282
  if (err.code === "EADDRINUSE") {
2724
4283
  log3.component("sentinel-server").error("Port already in use", { port: options.port });
2725
4284
  } else {
@@ -2730,63 +4289,6 @@ async function startServer(options) {
2730
4289
  });
2731
4290
  }
2732
4291
 
2733
- // src/config.ts
2734
- import * as fs5 from "fs";
2735
- import * as path6 from "path";
2736
- function writeConfig(projectDir, config) {
2737
- const filePath = path6.join(projectDir, ".sentinel.yaml");
2738
- const content = serializeSimpleYaml(config);
2739
- fs5.writeFileSync(filePath, content, "utf-8");
2740
- }
2741
- function serializeSimpleYaml(config) {
2742
- const lines = [];
2743
- lines.push(`# Sentinel Configuration`);
2744
- lines.push(`# Auto-generated \u2014 edit freely`);
2745
- lines.push("");
2746
- lines.push(`version: "${config.version}"`);
2747
- lines.push(`project: "${config.project}"`);
2748
- if (config.environment) {
2749
- lines.push(`environment: "${config.environment}"`);
2750
- }
2751
- if (config.symbols) {
2752
- lines.push("");
2753
- lines.push("symbols:");
2754
- for (const [key, values] of Object.entries(config.symbols)) {
2755
- if (values && values.length > 0) {
2756
- lines.push(` ${key}:`);
2757
- for (const v of values) {
2758
- lines.push(` - ${v}`);
2759
- }
2760
- }
2761
- }
2762
- }
2763
- if (config.routes && Object.keys(config.routes).length > 0) {
2764
- lines.push("");
2765
- lines.push("routes:");
2766
- for (const [route, symbol] of Object.entries(config.routes)) {
2767
- lines.push(` "${route}": ${symbol}`);
2768
- }
2769
- }
2770
- if (config.scrub) {
2771
- lines.push("");
2772
- lines.push("scrub:");
2773
- if (config.scrub.headers?.length) {
2774
- lines.push(" headers:");
2775
- for (const h of config.scrub.headers) {
2776
- lines.push(` - ${h}`);
2777
- }
2778
- }
2779
- if (config.scrub.fields?.length) {
2780
- lines.push(" fields:");
2781
- for (const f of config.scrub.fields) {
2782
- lines.push(` - ${f}`);
2783
- }
2784
- }
2785
- }
2786
- lines.push("");
2787
- return lines.join("\n");
2788
- }
2789
-
2790
4292
  // src/detector.ts
2791
4293
  import * as fs6 from "fs";
2792
4294
  import * as path7 from "path";