kachow 0.1.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.
Files changed (48) hide show
  1. package/README.md +77 -0
  2. package/_server/dist/app.js +130 -0
  3. package/_server/dist/db/index.js +50 -0
  4. package/_server/dist/db/schema.js +247 -0
  5. package/_server/dist/queues/ingestQueue.js +49 -0
  6. package/_server/dist/queues/redis.js +58 -0
  7. package/_server/dist/routes/agents.js +162 -0
  8. package/_server/dist/routes/architecture.js +88 -0
  9. package/_server/dist/routes/config.js +24 -0
  10. package/_server/dist/routes/github.js +158 -0
  11. package/_server/dist/routes/graph.js +112 -0
  12. package/_server/dist/routes/healing.js +137 -0
  13. package/_server/dist/routes/impact.js +100 -0
  14. package/_server/dist/routes/ingest.js +182 -0
  15. package/_server/dist/routes/manager.js +179 -0
  16. package/_server/dist/routes/notifications.js +85 -0
  17. package/_server/dist/routes/qa.js +68 -0
  18. package/_server/dist/routes/scanner.js +221 -0
  19. package/_server/dist/routes/stream.js +179 -0
  20. package/_server/dist/routes/webhooks.js +168 -0
  21. package/_server/dist/server.js +46 -0
  22. package/_server/dist/services/agentService.js +715 -0
  23. package/_server/dist/services/architectureService.js +172 -0
  24. package/_server/dist/services/demoSeed.js +181 -0
  25. package/_server/dist/services/graphLayout.js +102 -0
  26. package/_server/dist/services/graphService.js +532 -0
  27. package/_server/dist/services/healingService.js +253 -0
  28. package/_server/dist/services/impactService.js +304 -0
  29. package/_server/dist/services/ingestService.js +129 -0
  30. package/_server/dist/services/managerService.js +260 -0
  31. package/_server/dist/services/notificationService.js +283 -0
  32. package/_server/dist/services/qaService.js +413 -0
  33. package/_server/dist/services/scannerService.js +748 -0
  34. package/_server/dist/services/seedService.js +215 -0
  35. package/_server/dist/sse/sseManager.js +101 -0
  36. package/_server/dist/types/index.js +38 -0
  37. package/_server/dist/workers/ingestWorker.js +274 -0
  38. package/_server/public/assets/index-BTkbB_YF.js +4546 -0
  39. package/_server/public/assets/index-Bmh3jWBm.css +1 -0
  40. package/_server/public/favicon.ico +0 -0
  41. package/_server/public/images/glass-waves-bg.png +0 -0
  42. package/_server/public/index.html +29 -0
  43. package/_server/public/placeholder.svg +1 -0
  44. package/_server/public/robots.txt +14 -0
  45. package/dist/config.js +133 -0
  46. package/dist/index.js +510 -0
  47. package/dist/setup.js +223 -0
  48. package/package.json +62 -0
@@ -0,0 +1,532 @@
1
+ "use strict";
2
+ /**
3
+ * Graph read service — Phase 2.
4
+ * Pure read queries against SQLite. No mutations here.
5
+ * All write paths live in ingestWorker.ts.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.getNodes = getNodes;
9
+ exports.getEdges = getEdges;
10
+ exports.getNodeDetail = getNodeDetail;
11
+ exports.getCriticalIssues = getCriticalIssues;
12
+ exports.getSystemHealth = getSystemHealth;
13
+ exports.getSnapshot = getSnapshot;
14
+ exports.saveSnapshot = saveSnapshot;
15
+ const crypto_1 = require("crypto");
16
+ const index_js_1 = require("../db/index.js");
17
+ const graphLayout_js_1 = require("./graphLayout.js");
18
+ // ── Helpers ───────────────────────────────────────────────────────────────────
19
+ /**
20
+ * Parses a JSON endpoints string stored in the DB into a string array.
21
+ * Returns an empty array on any parse error.
22
+ *
23
+ * @param raw - JSON string from the DB or null
24
+ * @returns Parsed string array
25
+ */
26
+ function parseEndpoints(raw) {
27
+ if (!raw)
28
+ return [];
29
+ try {
30
+ const parsed = JSON.parse(raw);
31
+ return Array.isArray(parsed) ? parsed : [];
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
37
+ // ── Query functions ───────────────────────────────────────────────────────────
38
+ /**
39
+ * Returns all service nodes with computed layout coordinates and connection counts.
40
+ * When teamName is supplied only that team's services + their external references are returned.
41
+ * When teamId is supplied, filters by FK team_id for multi-tenant isolation.
42
+ * Used by GET /api/graph/nodes.
43
+ *
44
+ * @param teamName - Optional team name (label) to filter services by
45
+ * @param teamId - Optional team ID (FK) for multi-tenant scoping
46
+ * @returns Array of GraphNode objects sorted by health score descending
47
+ */
48
+ function getNodes(teamName, teamId) {
49
+ const db = (0, index_js_1.getDb)();
50
+ let rows;
51
+ if (teamId) {
52
+ // Multi-tenant: filter by FK
53
+ rows = db.prepare(`
54
+ SELECT s.id, s.name, s.team, s.language, s.health_score, s.health_tier,
55
+ s.repo_url, s.doc_coverage, s.test_coverage, s.last_ingested,
56
+ (SELECT COUNT(*) FROM dependencies WHERE source_id = s.id) +
57
+ (SELECT COUNT(*) FROM dependencies WHERE target_id = s.id) AS connection_count
58
+ FROM services s WHERE s.is_external = 0 AND s.team_id = ?
59
+ ORDER BY s.health_score DESC
60
+ `).all(teamId);
61
+ if (rows.length === 0) {
62
+ // fallback to all
63
+ rows = db.prepare(`
64
+ SELECT s.id, s.name, s.team, s.language, s.health_score, s.health_tier,
65
+ s.repo_url, s.doc_coverage, s.test_coverage, s.last_ingested,
66
+ (SELECT COUNT(*) FROM dependencies WHERE source_id = s.id) +
67
+ (SELECT COUNT(*) FROM dependencies WHERE target_id = s.id) AS connection_count
68
+ FROM services s WHERE s.is_external = 0
69
+ ORDER BY s.health_score DESC
70
+ `).all();
71
+ }
72
+ }
73
+ else if (teamName) {
74
+ rows = db.prepare(`
75
+ SELECT s.id, s.name, s.team, s.language, s.health_score, s.health_tier,
76
+ s.repo_url, s.doc_coverage, s.test_coverage, s.last_ingested,
77
+ (SELECT COUNT(*) FROM dependencies WHERE source_id = s.id) +
78
+ (SELECT COUNT(*) FROM dependencies WHERE target_id = s.id) AS connection_count
79
+ FROM services s WHERE s.is_external = 0 AND s.team = ?
80
+ ORDER BY s.health_score DESC
81
+ `).all(teamName);
82
+ if (rows.length === 0) {
83
+ rows = db.prepare(`
84
+ SELECT s.id, s.name, s.team, s.language, s.health_score, s.health_tier,
85
+ s.repo_url, s.doc_coverage, s.test_coverage, s.last_ingested,
86
+ (SELECT COUNT(*) FROM dependencies WHERE source_id = s.id) +
87
+ (SELECT COUNT(*) FROM dependencies WHERE target_id = s.id) AS connection_count
88
+ FROM services s WHERE s.is_external = 0
89
+ ORDER BY s.health_score DESC
90
+ `).all();
91
+ }
92
+ }
93
+ else {
94
+ rows = db.prepare(`
95
+ SELECT s.id, s.name, s.team, s.language, s.health_score, s.health_tier,
96
+ s.repo_url, s.doc_coverage, s.test_coverage, s.last_ingested,
97
+ (SELECT COUNT(*) FROM dependencies WHERE source_id = s.id) +
98
+ (SELECT COUNT(*) FROM dependencies WHERE target_id = s.id) AS connection_count
99
+ FROM services s WHERE s.is_external = 0
100
+ ORDER BY s.health_score DESC
101
+ `).all();
102
+ }
103
+ const internalIds = rows.map(r => r.id);
104
+ const idPlaceholders = internalIds.length ? internalIds.map(() => '?').join(',') : "''";
105
+ // External nodes that are referenced by the (filtered) internal services
106
+ const externalRows = db.prepare(`
107
+ SELECT
108
+ s.id, s.name, s.team, s.language, s.health_score, s.health_tier,
109
+ s.repo_url, s.doc_coverage, s.test_coverage, s.last_ingested,
110
+ (SELECT COUNT(*) FROM dependencies WHERE source_id = s.id) +
111
+ (SELECT COUNT(*) FROM dependencies WHERE target_id = s.id) AS connection_count
112
+ FROM services s
113
+ WHERE s.is_external = 1
114
+ AND (
115
+ EXISTS (SELECT 1 FROM dependencies WHERE target_id = s.id AND source_id IN (${idPlaceholders}))
116
+ OR EXISTS (SELECT 1 FROM dependencies WHERE source_id = s.id AND target_id IN (${idPlaceholders}))
117
+ )
118
+ ORDER BY s.name
119
+ `).all(...internalIds, ...internalIds);
120
+ const edgeRows = db.prepare('SELECT source_id, target_id FROM dependencies').all();
121
+ const layout = (0, graphLayout_js_1.computeLayout)(rows, edgeRows);
122
+ // Position external nodes in a dedicated far-right column
123
+ const EXT_X = 1350;
124
+ const extStep = externalRows.length > 0 ? 700 / (externalRows.length + 1) : 350;
125
+ const result = rows.map((r) => ({
126
+ id: r.id,
127
+ name: r.name,
128
+ healthScore: r.health_score,
129
+ healthTier: r.health_tier,
130
+ team: r.team,
131
+ language: r.language,
132
+ connectionCount: r.connection_count,
133
+ x: layout.get(r.id)?.x ?? 700,
134
+ y: layout.get(r.id)?.y ?? 350,
135
+ }));
136
+ externalRows.forEach((r, i) => {
137
+ result.push({
138
+ id: r.id,
139
+ name: r.name,
140
+ healthScore: r.health_score,
141
+ healthTier: r.health_tier,
142
+ team: r.team,
143
+ language: r.language,
144
+ connectionCount: r.connection_count,
145
+ x: EXT_X,
146
+ y: Math.round(extStep * (i + 1)),
147
+ isExternal: true,
148
+ });
149
+ });
150
+ return result;
151
+ }
152
+ /**
153
+ * Returns all dependency edges in the graph.
154
+ * When teamName is supplied only edges where at least one endpoint belongs to
155
+ * that team are returned.
156
+ * Used by GET /api/graph/edges.
157
+ *
158
+ * @param teamName - Optional team name (label) to filter edges by
159
+ * @param teamId - Optional team ID (FK) for multi-tenant scoping
160
+ * @returns Array of GraphEdge objects
161
+ */
162
+ function getEdges(teamName, teamId) {
163
+ const db = (0, index_js_1.getDb)();
164
+ let rows;
165
+ if (teamId) {
166
+ rows = db.prepare(`
167
+ SELECT d.id, d.source_id, d.target_id, d.type, d.endpoints, d.strength
168
+ FROM dependencies d
169
+ WHERE d.source_id IN (SELECT id FROM services WHERE team_id = ?)
170
+ OR d.target_id IN (SELECT id FROM services WHERE team_id = ?)
171
+ `).all(teamId, teamId);
172
+ if (rows.length === 0) {
173
+ rows = db.prepare('SELECT id, source_id, target_id, type, endpoints, strength FROM dependencies').all();
174
+ }
175
+ }
176
+ else if (teamName) {
177
+ rows = db.prepare(`
178
+ SELECT d.id, d.source_id, d.target_id, d.type, d.endpoints, d.strength
179
+ FROM dependencies d
180
+ WHERE d.source_id IN (SELECT id FROM services WHERE team = ?)
181
+ OR d.target_id IN (SELECT id FROM services WHERE team = ?)
182
+ `).all(teamName, teamName);
183
+ if (rows.length === 0) {
184
+ rows = db.prepare('SELECT id, source_id, target_id, type, endpoints, strength FROM dependencies').all();
185
+ }
186
+ }
187
+ else {
188
+ rows = db.prepare('SELECT id, source_id, target_id, type, endpoints, strength FROM dependencies').all();
189
+ }
190
+ return rows.map((r) => ({
191
+ id: r.id,
192
+ sourceId: r.source_id,
193
+ targetId: r.target_id,
194
+ type: r.type,
195
+ endpoints: parseEndpoints(r.endpoints),
196
+ strength: r.strength,
197
+ }));
198
+ }
199
+ /**
200
+ * Returns full detail for a single service node.
201
+ * Includes endpoints, incidents, inbound/outbound dependencies, and recent activity.
202
+ * Used by GET /api/graph/node/:id.
203
+ *
204
+ * @param id - Service node ID
205
+ * @returns GraphNodeDetail or null if not found
206
+ */
207
+ function getNodeDetail(id) {
208
+ const db = (0, index_js_1.getDb)();
209
+ const svcRow = db.prepare(`
210
+ SELECT s.*,
211
+ (SELECT COUNT(*) FROM dependencies WHERE source_id = s.id) +
212
+ (SELECT COUNT(*) FROM dependencies WHERE target_id = s.id) AS connection_count
213
+ FROM services s WHERE s.id = ?
214
+ `).get(id);
215
+ if (!svcRow)
216
+ return null;
217
+ const edgeRows = db.prepare('SELECT source_id, target_id FROM dependencies').all();
218
+ const layout = (0, graphLayout_js_1.computeLayout)(db.prepare('SELECT id FROM services').all(), edgeRows);
219
+ const endpoints = db.prepare('SELECT id, method, path, deprecated, has_spec FROM endpoints WHERE service_id = ?').all(id);
220
+ const incidents = db.prepare(`
221
+ SELECT id, severity, title, status, occurred_at, resolved_at
222
+ FROM incidents WHERE service_id = ? ORDER BY occurred_at DESC LIMIT 10
223
+ `).all(id);
224
+ const dependsOn = db.prepare(`
225
+ SELECT d.id, d.source_id, d.target_id, d.type, d.endpoints, d.strength,
226
+ s.id as peer_id, s.name as peer_name
227
+ FROM dependencies d JOIN services s ON s.id = d.target_id
228
+ WHERE d.source_id = ?
229
+ `).all(id);
230
+ const dependedOnBy = db.prepare(`
231
+ SELECT d.id, d.source_id, d.target_id, d.type, d.endpoints, d.strength,
232
+ s.id as peer_id, s.name as peer_name
233
+ FROM dependencies d JOIN services s ON s.id = d.source_id
234
+ WHERE d.target_id = ?
235
+ `).all(id);
236
+ const recentActivity = db.prepare(`
237
+ SELECT id, type, title, detail, created_at FROM activity_feed
238
+ WHERE service_id = ? ORDER BY created_at DESC LIMIT 10
239
+ `).all(id);
240
+ return {
241
+ service: {
242
+ id: svcRow.id, name: svcRow.name,
243
+ healthScore: svcRow.health_score, healthTier: svcRow.health_tier,
244
+ team: svcRow.team, language: svcRow.language,
245
+ connectionCount: svcRow.connection_count,
246
+ x: layout.get(id)?.x ?? 700, y: layout.get(id)?.y ?? 350,
247
+ repoUrl: svcRow.repo_url, docCoverage: svcRow.doc_coverage,
248
+ testCoverage: svcRow.test_coverage, lastIngested: svcRow.last_ingested,
249
+ },
250
+ endpoints: endpoints.map((e) => ({
251
+ id: e.id, method: e.method, path: e.path,
252
+ deprecated: e.deprecated === 1, hasSpec: e.has_spec === 1,
253
+ })),
254
+ incidents: incidents.map((i) => ({
255
+ id: i.id, severity: i.severity, title: i.title, status: i.status,
256
+ occurredAt: i.occurred_at, resolvedAt: i.resolved_at,
257
+ })),
258
+ dependsOn: dependsOn.map((d) => ({
259
+ serviceId: d.peer_id, serviceName: d.peer_name,
260
+ type: d.type, endpoints: parseEndpoints(d.endpoints),
261
+ strength: d.strength,
262
+ })),
263
+ dependedOnBy: dependedOnBy.map((d) => ({
264
+ serviceId: d.peer_id, serviceName: d.peer_name,
265
+ type: d.type, endpoints: parseEndpoints(d.endpoints),
266
+ strength: d.strength,
267
+ })),
268
+ recentActivity: recentActivity.map((a) => ({
269
+ id: a.id, type: a.type, title: a.title, detail: a.detail, createdAt: a.created_at,
270
+ })),
271
+ };
272
+ }
273
+ /**
274
+ * Aggregates critical issues from three sources:
275
+ * 1. Agent KG — healthIssues per node + selfHealingSuggestions
276
+ * 2. healing_prs table — issues detected by self-healing agents
277
+ * 3. incidents table — open incidents
278
+ *
279
+ * Returns a flat CriticalIssue[] sorted by severity (critical → warning → info).
280
+ */
281
+ function getCriticalIssues(teamId) {
282
+ const db = (0, index_js_1.getDb)();
283
+ const issues = [];
284
+ // ── 1) Agent Knowledge Graph health issues + self-healing ─────────────────
285
+ // Service lookup for enrichment (scoped by team when provided)
286
+ const svcMap = new Map();
287
+ const svcQuery = teamId
288
+ ? 'SELECT id, name, health_score, health_tier FROM services WHERE is_external = 0 AND team_id = ?'
289
+ : 'SELECT id, name, health_score, health_tier FROM services WHERE is_external = 0';
290
+ const svcRows = teamId
291
+ ? db.prepare(svcQuery).all(teamId)
292
+ : db.prepare(svcQuery).all();
293
+ for (const s of svcRows)
294
+ svcMap.set(s.id, { name: s.name, healthScore: s.health_score, healthTier: s.health_tier });
295
+ // Also build a name→id reverse lookup (agent KG uses service names, not IDs)
296
+ const nameToId = new Map();
297
+ for (const s of svcRows)
298
+ nameToId.set(s.name.toLowerCase(), s.id);
299
+ // Read the latest agent KG from knowledge_graph table (team-scoped when provided)
300
+ const kgQuery = teamId
301
+ ? `SELECT nodes, self_healing FROM knowledge_graph WHERE team_id = ? ORDER BY created_at DESC LIMIT 1`
302
+ : `SELECT nodes, self_healing FROM knowledge_graph ORDER BY created_at DESC LIMIT 1`;
303
+ const kgRow = teamId
304
+ ? db.prepare(kgQuery).get(teamId)
305
+ : db.prepare(kgQuery).get();
306
+ if (kgRow) {
307
+ try {
308
+ const kgNodes = JSON.parse(kgRow.nodes);
309
+ const selfHealing = JSON.parse(kgRow.self_healing);
310
+ // Per-node health issues
311
+ for (const n of kgNodes) {
312
+ const svcId = n.id ?? nameToId.get(n.name.toLowerCase());
313
+ const svc = svcId ? svcMap.get(svcId) : undefined;
314
+ for (const issue of n.healthIssues ?? []) {
315
+ const severity = classifySeverity(issue);
316
+ issues.push({
317
+ id: `agent-hi-${svcId ?? n.name}-${issues.length}`,
318
+ serviceId: svcId ?? n.name,
319
+ serviceName: svc?.name ?? n.name,
320
+ healthScore: svc?.healthScore ?? 0,
321
+ healthTier: svc?.healthTier ?? 'warning',
322
+ severity,
323
+ category: classifyCategory(issue),
324
+ title: issue.length > 80 ? issue.slice(0, 77) + '…' : issue,
325
+ description: issue,
326
+ file: null,
327
+ line: null,
328
+ suggestedFix: null,
329
+ source: 'agent',
330
+ });
331
+ }
332
+ }
333
+ // Self-healing suggestions (richer — include file/line/fix)
334
+ for (const sh of selfHealing) {
335
+ const svcId = sh.serviceId ?? nameToId.get(sh.serviceName.toLowerCase());
336
+ const svc = svcId ? svcMap.get(svcId) : undefined;
337
+ issues.push({
338
+ id: `agent-sh-${svcId ?? sh.serviceName}-${issues.length}`,
339
+ serviceId: svcId ?? sh.serviceName,
340
+ serviceName: svc?.name ?? sh.serviceName,
341
+ healthScore: svc?.healthScore ?? 0,
342
+ healthTier: svc?.healthTier ?? 'warning',
343
+ severity: (sh.severity === 'critical' || sh.severity === 'warning' || sh.severity === 'info')
344
+ ? sh.severity : 'warning',
345
+ category: classifyCategory(sh.issue),
346
+ title: sh.issue.length > 80 ? sh.issue.slice(0, 77) + '…' : sh.issue,
347
+ description: sh.issue,
348
+ file: sh.file || null,
349
+ line: sh.line || null,
350
+ suggestedFix: sh.suggestion || null,
351
+ source: 'agent',
352
+ });
353
+ }
354
+ }
355
+ catch { /* ignore malformed KG */ }
356
+ }
357
+ const healingQuery = teamId
358
+ ? `SELECT id, service_id, issue_type, explanation, status
359
+ FROM healing_prs WHERE status IN ('detected', 'open') AND team_id = ?`
360
+ : `SELECT id, service_id, issue_type, explanation, status
361
+ FROM healing_prs WHERE status IN ('detected', 'open')`;
362
+ const healingRows = teamId
363
+ ? db.prepare(healingQuery).all(teamId)
364
+ : db.prepare(healingQuery).all();
365
+ for (const h of healingRows) {
366
+ const svc = svcMap.get(h.service_id);
367
+ // Avoid duplicate if agent KG already has the same issue text
368
+ const dup = issues.some(i => i.serviceId === h.service_id && i.description === h.explanation);
369
+ if (!dup) {
370
+ issues.push({
371
+ id: `heal-${h.id}`,
372
+ serviceId: h.service_id,
373
+ serviceName: svc?.name ?? h.service_id,
374
+ healthScore: svc?.healthScore ?? 0,
375
+ healthTier: svc?.healthTier ?? 'warning',
376
+ severity: classifySeverity(h.issue_type + ' ' + h.explanation),
377
+ category: classifyCategory(h.issue_type),
378
+ title: h.issue_type,
379
+ description: h.explanation,
380
+ file: null,
381
+ line: null,
382
+ suggestedFix: null,
383
+ source: 'healing',
384
+ });
385
+ }
386
+ }
387
+ // ── 3) Open incidents (scoped through service FK when teamId provided) ──
388
+ const incidentQuery = teamId
389
+ ? `SELECT i.id, i.service_id, i.severity, i.title, i.status, i.occurred_at
390
+ FROM incidents i
391
+ JOIN services s ON s.id = i.service_id
392
+ WHERE i.status != 'resolved' AND s.team_id = ?
393
+ ORDER BY i.occurred_at DESC LIMIT 30`
394
+ : `SELECT id, service_id, severity, title, status, occurred_at
395
+ FROM incidents WHERE status != 'resolved' ORDER BY occurred_at DESC LIMIT 30`;
396
+ const incidentRows = teamId
397
+ ? db.prepare(incidentQuery).all(teamId)
398
+ : db.prepare(incidentQuery).all();
399
+ for (const inc of incidentRows) {
400
+ const svc = svcMap.get(inc.service_id);
401
+ issues.push({
402
+ id: `inc-${inc.id}`,
403
+ serviceId: inc.service_id,
404
+ serviceName: svc?.name ?? inc.service_id,
405
+ healthScore: svc?.healthScore ?? 0,
406
+ healthTier: svc?.healthTier ?? 'warning',
407
+ severity: inc.severity === 'critical' ? 'critical' : inc.severity === 'warning' ? 'warning' : 'info',
408
+ category: 'reliability',
409
+ title: inc.title,
410
+ description: `${inc.title} — ${inc.status} since ${inc.occurred_at}`,
411
+ file: null,
412
+ line: null,
413
+ suggestedFix: null,
414
+ source: 'incident',
415
+ });
416
+ }
417
+ // ── Sort: critical first, then warning, then info ─────────────────────────
418
+ const ORDER = { critical: 0, warning: 1, info: 2 };
419
+ issues.sort((a, b) => (ORDER[a.severity] ?? 2) - (ORDER[b.severity] ?? 2));
420
+ return issues;
421
+ }
422
+ // Heuristic severity from free-text
423
+ function classifySeverity(text) {
424
+ const t = text.toLowerCase();
425
+ if (/hardcoded.*(key|secret|password|token)|sql.?inject|xss|remote.?code|auth.?bypass|critical/i.test(t))
426
+ return 'critical';
427
+ if (/missing.*header|no.*(rate.?limit|input.?valid|error.?hand)|warning|deprecated/i.test(t))
428
+ return 'warning';
429
+ return 'info';
430
+ }
431
+ // Heuristic category from free-text
432
+ function classifyCategory(text) {
433
+ const t = text.toLowerCase();
434
+ if (/secur|key|secret|token|inject|xss|auth|password|cors|header/i.test(t))
435
+ return 'security';
436
+ if (/perf|latency|cache|throughput|slow|memory|cpu/i.test(t))
437
+ return 'performance';
438
+ if (/reliab|error.?hand|circuit|retry|timeout|crash|incident|health/i.test(t))
439
+ return 'reliability';
440
+ return 'quality';
441
+ }
442
+ /**
443
+ * Returns aggregate health metrics for the entire system.
444
+ * Used by GET /api/graph/health — polled every 30s by the frontend.
445
+ *
446
+ * @returns SystemHealth object with score breakdown
447
+ */
448
+ function getSystemHealth(teamId) {
449
+ const db = (0, index_js_1.getDb)();
450
+ const baseWhere = teamId
451
+ ? 'WHERE is_external = 0 AND team_id = ?'
452
+ : 'WHERE is_external = 0';
453
+ const params = teamId ? [teamId] : [];
454
+ const row = db.prepare(`
455
+ SELECT
456
+ ROUND(AVG(health_score), 1) AS overall,
457
+ COUNT(*) AS total,
458
+ SUM(CASE WHEN health_tier = 'critical' THEN 1 ELSE 0 END) AS critical,
459
+ SUM(CASE WHEN health_tier = 'warning' THEN 1 ELSE 0 END) AS warning,
460
+ SUM(CASE WHEN health_tier = 'healthy' THEN 1 ELSE 0 END) AS healthy,
461
+ MAX(last_ingested) AS last_updated
462
+ FROM services
463
+ ${baseWhere}
464
+ `).get(...params);
465
+ const edgeCount = teamId
466
+ ? (db.prepare(`SELECT COUNT(*) AS cnt FROM dependencies d
467
+ WHERE EXISTS (SELECT 1 FROM services s WHERE s.id = d.source_id AND s.team_id = ?)
468
+ OR EXISTS (SELECT 1 FROM services s WHERE s.id = d.target_id AND s.team_id = ?)`).get(teamId, teamId)?.cnt ?? 0)
469
+ : (db.prepare(`SELECT COUNT(*) AS cnt FROM dependencies`).get()?.cnt ?? 0);
470
+ return {
471
+ overallScore: row?.overall ?? 0,
472
+ totalServices: row?.total ?? 0,
473
+ critical: row?.critical ?? 0,
474
+ warning: row?.warning ?? 0,
475
+ healthy: row?.healthy ?? 0,
476
+ totalEdges: edgeCount,
477
+ lastUpdated: row?.last_updated ?? null,
478
+ };
479
+ }
480
+ /**
481
+ * Retrieves a stored graph snapshot by version string.
482
+ * Version 'latest' returns the most recently saved snapshot.
483
+ * Used by GET /api/graph/snapshot/:version.
484
+ *
485
+ * @param version - Git tag, ISO timestamp string, or 'latest'
486
+ * @returns GraphSnapshot or null if not found
487
+ */
488
+ function getSnapshot(version) {
489
+ const db = (0, index_js_1.getDb)();
490
+ let row;
491
+ if (version === 'latest') {
492
+ row = db.prepare('SELECT nodes, edges, version, created_at FROM snapshots ORDER BY created_at DESC LIMIT 1').get();
493
+ }
494
+ else {
495
+ row = db.prepare('SELECT nodes, edges, version, created_at FROM snapshots WHERE version = ?').get(version);
496
+ }
497
+ if (!row)
498
+ return null;
499
+ try {
500
+ return {
501
+ nodes: JSON.parse(row.nodes),
502
+ edges: JSON.parse(row.edges),
503
+ version: row.version,
504
+ timestamp: row.created_at,
505
+ };
506
+ }
507
+ catch {
508
+ return null;
509
+ }
510
+ }
511
+ /**
512
+ * Saves the current graph state as a named snapshot.
513
+ * Called by the ingest worker after a successful full scan.
514
+ *
515
+ * @param version - Snapshot version label (git tag or ISO timestamp)
516
+ * @param teamId - Optional team ID for multi-tenant scoping
517
+ * @returns void
518
+ */
519
+ function saveSnapshot(version, teamId) {
520
+ const db = (0, index_js_1.getDb)();
521
+ const nodes = teamId ? getNodes(undefined, teamId) : getNodes();
522
+ const edges = getEdges();
523
+ db.prepare(`
524
+ INSERT INTO snapshots (id, version, team_id, nodes, edges)
525
+ VALUES (?, ?, ?, ?, ?)
526
+ ON CONFLICT(version, team_id) DO UPDATE SET
527
+ nodes = excluded.nodes,
528
+ edges = excluded.edges,
529
+ created_at = datetime('now')
530
+ `).run((0, crypto_1.randomUUID)(), version, teamId ?? null, JSON.stringify(nodes), JSON.stringify(edges));
531
+ }
532
+ //# sourceMappingURL=graphService.js.map