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,413 @@
1
+ "use strict";
2
+ /**
3
+ * Q&A Engine — Phase 3.
4
+ * Graph-aware RAG using OpenAI GPT-4o.
5
+ * Builds rich context from the knowledge graph, queries OpenAI,
6
+ * and returns structured answers with file:line citations.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.queryKnowledgeGraph = queryKnowledgeGraph;
13
+ exports.getQAHistory = getQAHistory;
14
+ exports.getQASuggestions = getQASuggestions;
15
+ const openai_1 = __importDefault(require("openai"));
16
+ const crypto_1 = require("crypto");
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const index_js_1 = require("../db/index.js");
20
+ const graphService_js_1 = require("./graphService.js");
21
+ // ── File-tree helpers ─────────────────────────────────────────────────────────
22
+ /** Walk a directory and return relative paths of source files (max 200). */
23
+ function walkSourceFiles(dir, base = dir, results = []) {
24
+ if (results.length >= 200)
25
+ return results;
26
+ if (!fs_1.default.existsSync(dir))
27
+ return results;
28
+ for (const entry of fs_1.default.readdirSync(dir)) {
29
+ if (['node_modules', '.git', 'dist', 'build', '.next'].includes(entry))
30
+ continue;
31
+ const full = path_1.default.join(dir, entry);
32
+ const rel = path_1.default.relative(base, full);
33
+ try {
34
+ if (fs_1.default.statSync(full).isDirectory()) {
35
+ walkSourceFiles(full, base, results);
36
+ }
37
+ else if (/\.(ts|tsx|js|jsx|py|go|java)$/.test(entry)) {
38
+ results.push(rel);
39
+ }
40
+ }
41
+ catch { /* skip */ }
42
+ }
43
+ return results;
44
+ }
45
+ /**
46
+ * Given a GPT-hallucinated file path (e.g. "src/routes.ts") and the real list
47
+ * of files in the service directory, return the closest real match.
48
+ * Strategy: exact → basename match → longest common-suffix match.
49
+ */
50
+ function bestFileMatch(hallucinated, realFiles) {
51
+ if (realFiles.includes(hallucinated))
52
+ return hallucinated;
53
+ const base = path_1.default.basename(hallucinated); // e.g. "routes.ts"
54
+ const baseName = base.replace(/\.[^.]+$/, ''); // e.g. "routes"
55
+ const ext = path_1.default.extname(base); // e.g. ".ts"
56
+ // 1. Exact basename match
57
+ const exactBase = realFiles.filter(f => path_1.default.basename(f) === base);
58
+ if (exactBase.length === 1)
59
+ return exactBase[0];
60
+ // 2. Same name, any extension (e.g. routes.js → routes.ts)
61
+ const sameNameAnyExt = realFiles.filter(f => path_1.default.basename(f, path_1.default.extname(f)) === baseName);
62
+ if (sameNameAnyExt.length === 1)
63
+ return sameNameAnyExt[0];
64
+ // 3. Filename contains the base name (e.g. "routes" → "routes/blog.ts")
65
+ const contains = realFiles.filter(f => {
66
+ const parts = f.split('/');
67
+ return parts.some(p => p.replace(/\.[^.]+$/, '') === baseName);
68
+ });
69
+ if (contains.length >= 1)
70
+ return contains[0];
71
+ // 4. Any file with matching extension, prefer shortest path
72
+ const sameExt = realFiles.filter(f => path_1.default.extname(f) === ext)
73
+ .sort((a, b) => a.length - b.length);
74
+ return sameExt[0] ?? hallucinated;
75
+ }
76
+ /**
77
+ * Builds a rich textual context from the current knowledge graph.
78
+ * Includes service health, dependencies, endpoints, incidents, and recent activity.
79
+ */
80
+ function buildGraphContext() {
81
+ const nodes = (0, graphService_js_1.getNodes)();
82
+ const edges = (0, graphService_js_1.getEdges)();
83
+ const health = (0, graphService_js_1.getSystemHealth)();
84
+ const db = (0, index_js_1.getDb)();
85
+ const serviceLines = nodes.map(n => {
86
+ const outEdges = edges.filter(e => e.sourceId === n.id);
87
+ const inEdges = edges.filter(e => e.targetId === n.id);
88
+ const incidents = db.prepare(`
89
+ SELECT severity, title FROM incidents
90
+ WHERE service_id = ? AND status = 'open'
91
+ ORDER BY occurred_at DESC LIMIT 3
92
+ `).all(n.id);
93
+ const endpoints = db.prepare(`
94
+ SELECT method, path FROM endpoints WHERE service_id = ? LIMIT 8
95
+ `).all(n.id);
96
+ const allNodes = nodes;
97
+ const calls = outEdges.map(e => {
98
+ const target = allNodes.find(x => x.id === e.targetId);
99
+ const ep = e.endpoints.slice(0, 2).join(', ');
100
+ return `→ ${target?.name ?? e.targetId} via ${e.type}${ep ? ` [${ep}]` : ''}`;
101
+ });
102
+ const calledBy = inEdges.map(e => {
103
+ const src = allNodes.find(x => x.id === e.sourceId);
104
+ return `← ${src?.name ?? e.sourceId} (${e.type})`;
105
+ });
106
+ const incidentStr = incidents.map(i => `[${i.severity}] ${i.title}`).join('; ') || 'none';
107
+ const epStr = endpoints.map(e => `${e.method} ${e.path}`).join(', ') || 'none';
108
+ const svcRow = db.prepare(`SELECT repo_url, repo_subpath FROM services WHERE id = ? LIMIT 1`).get(n.id);
109
+ // Build real file list for this service
110
+ let fileListStr = '';
111
+ if (svcRow?.repo_url) {
112
+ // Reconstruct local path from repo_url and repo_subpath
113
+ // We stored repoPath in the ingest job but not in DB — infer from /tmp or known prefixes
114
+ const knownBases = ['/tmp', '/Users'];
115
+ const repoName = svcRow.repo_url.split('/').pop() ?? '';
116
+ let serviceDir = null;
117
+ for (const base of knownBases) {
118
+ const candidate = svcRow.repo_subpath
119
+ ? path_1.default.join(base, repoName, svcRow.repo_subpath)
120
+ : path_1.default.join(base, repoName);
121
+ if (fs_1.default.existsSync(candidate)) {
122
+ serviceDir = candidate;
123
+ break;
124
+ }
125
+ // Also try under /tmp directly
126
+ const candidate2 = svcRow.repo_subpath
127
+ ? path_1.default.join('/tmp', `${repoName}*`, svcRow.repo_subpath)
128
+ : null;
129
+ if (candidate2 && fs_1.default.existsSync(candidate2)) {
130
+ serviceDir = candidate2;
131
+ break;
132
+ }
133
+ }
134
+ // Brute-force find in /tmp if name matches
135
+ if (!serviceDir) {
136
+ try {
137
+ const tmpDirs = fs_1.default.readdirSync('/tmp').filter(d => d.startsWith(repoName.replace(/[-_]/g, '-').slice(0, 12)));
138
+ for (const d of tmpDirs) {
139
+ const p2 = svcRow.repo_subpath ? path_1.default.join('/tmp', d, svcRow.repo_subpath) : path_1.default.join('/tmp', d);
140
+ if (fs_1.default.existsSync(p2)) {
141
+ serviceDir = p2;
142
+ break;
143
+ }
144
+ }
145
+ }
146
+ catch { /* ignore */ }
147
+ }
148
+ if (serviceDir) {
149
+ const files = walkSourceFiles(serviceDir).slice(0, 30);
150
+ if (files.length)
151
+ fileListStr = `\n Source files: ${files.join(', ')}`;
152
+ }
153
+ }
154
+ return [
155
+ `SERVICE: ${n.name}`,
156
+ ` id=${n.id} | health=${n.healthScore}/100 (${n.healthTier}) | lang=${n.language ?? 'unknown'} | team=${n.team ?? 'unassigned'}`,
157
+ ` Endpoints: ${epStr}`,
158
+ ` Calls: ${calls.join(', ') || 'none'}`,
159
+ ` Called by: ${calledBy.join(', ') || 'none'}`,
160
+ ` Open incidents: ${incidentStr}`,
161
+ fileListStr,
162
+ ].filter(Boolean).join('\n');
163
+ }).join('\n\n');
164
+ const recentActivity = db.prepare(`
165
+ SELECT type, title, detail, created_at FROM activity_feed
166
+ ORDER BY created_at DESC LIMIT 15
167
+ `).all();
168
+ const activityStr = recentActivity
169
+ .map(a => `[${a.created_at}] ${a.type}: ${a.title}${a.detail ? ` — ${a.detail}` : ''}`)
170
+ .join('\n');
171
+ return [
172
+ '=== SYSTEM HEALTH ===',
173
+ `Overall: ${health.overallScore}/100 | Services: ${health.totalServices} | Edges: ${health.totalEdges}`,
174
+ `Critical: ${health.critical} | Warning: ${health.warning} | Healthy: ${health.healthy}`,
175
+ '',
176
+ '=== SERVICES ===',
177
+ serviceLines || 'No services ingested yet.',
178
+ '',
179
+ '=== RECENT ACTIVITY ===',
180
+ activityStr || 'No recent activity.',
181
+ ].join('\n');
182
+ }
183
+ // ── Main query function ───────────────────────────────────────────────────────
184
+ /**
185
+ * Queries the knowledge graph using OpenAI GPT-4o.
186
+ * Builds graph context, constructs prompt, and returns structured answer with citations.
187
+ *
188
+ * @param question - Natural language question from the engineer
189
+ * @returns QAResult with answer, citations, related nodes, and follow-up suggestions
190
+ */
191
+ async function queryKnowledgeGraph(question) {
192
+ const apiKey = process.env.OPENAI_API_KEY;
193
+ if (!apiKey) {
194
+ throw new Error('OPENAI_API_KEY not set. Add it to .env to use Q&A.');
195
+ }
196
+ const openai = new openai_1.default({ apiKey });
197
+ const context = buildGraphContext();
198
+ const db = (0, index_js_1.getDb)();
199
+ const prompt = `You are KA-CHOW, an AI assistant embedded in a living knowledge graph for engineering teams.
200
+ You have real-time visibility into this system's microservice architecture.
201
+
202
+ ${context}
203
+
204
+ Answer the engineer's question with precision. Reference actual service names, health scores, and dependency chains from the context above.
205
+
206
+ RESPOND WITH ONLY VALID JSON — no markdown, no explanation outside the JSON:
207
+ {
208
+ "answer": "3-5 sentence answer referencing specific services by name with concrete details",
209
+ "citations": [
210
+ {
211
+ "file": "path/from/service-name/realistic-filename.ts",
212
+ "line": <realistic line number 1-300>,
213
+ "snippet": "relevant code pattern or config that supports the answer"
214
+ }
215
+ ],
216
+ "relatedNodes": ["exact-service-id-from-context"],
217
+ "suggestions": ["Follow-up question 1?", "Follow-up question 2?", "Follow-up question 3?"]
218
+ }
219
+
220
+ Rules:
221
+ - citations: use ONLY the exact file paths listed under "Source files" for that service (e.g. if source files lists "src/routes/blog.ts" use "blog-service/src/routes/blog.ts"). Do NOT invent paths.
222
+ - relatedNodes: use EXACT service id strings from the SERVICE lines above (e.g. "def53e95f1fc")
223
+ - suggestions: 3 highly relevant follow-up questions
224
+ - Be specific and actionable. Do not make up metrics not in the context.
225
+
226
+ Engineer's question: ${question}`;
227
+ const completion = await openai.chat.completions.create({
228
+ model: 'gpt-4o',
229
+ messages: [{ role: 'user', content: prompt }],
230
+ response_format: { type: 'json_object' },
231
+ temperature: 0.3,
232
+ max_tokens: 1500,
233
+ });
234
+ const raw = completion.choices[0]?.message?.content ?? '{}';
235
+ let parsed = {};
236
+ try {
237
+ parsed = JSON.parse(raw);
238
+ }
239
+ catch {
240
+ parsed = { answer: raw };
241
+ }
242
+ const rawCitations = (parsed.citations ?? []).slice(0, 5);
243
+ // ── Resolve citations: correct hallucinated paths + build URLs ─────────────
244
+ //
245
+ // Strategy:
246
+ // 1. Find the service row by name/id so we have repo_url + repo_subpath
247
+ // 2. Reconstruct the local directory by scanning known clone locations
248
+ // 3. Fuzzy-match the GPT-hallucinated file path against real files on disk
249
+ // 4. Build a correct GitHub URL using repo_url + repo_subpath (relative)
250
+ // 5. Also build a vscode:// URL so the frontend can open the file locally
251
+ /** Try to find the local clone directory for a given repo_url + repo_subpath */
252
+ function findLocalServiceDir(repoUrl, subpath) {
253
+ if (!repoUrl)
254
+ return null;
255
+ const repoName = repoUrl.split('/').pop() ?? '';
256
+ // Check common clone locations
257
+ const baseDirs = ['/tmp', '/private/tmp', '/Users'];
258
+ for (const base of baseDirs) {
259
+ try {
260
+ if (base === '/Users') {
261
+ // Don't walk all of /Users — skip for now
262
+ continue;
263
+ }
264
+ const entries = fs_1.default.readdirSync(base);
265
+ for (const entry of entries) {
266
+ if (!entry.toLowerCase().includes(repoName.toLowerCase().slice(0, 8)))
267
+ continue;
268
+ const repoRoot = path_1.default.join(base, entry);
269
+ const candidate = subpath ? path_1.default.join(repoRoot, subpath) : repoRoot;
270
+ if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isDirectory()) {
271
+ return candidate;
272
+ }
273
+ }
274
+ }
275
+ catch { /* skip */ }
276
+ }
277
+ return null;
278
+ }
279
+ const citations = rawCitations.map((cite) => {
280
+ const parts = cite.file.split('/');
281
+ let serviceName = parts[0];
282
+ let gptFilePath = parts.slice(1).join('/');
283
+ // Find the service row by name/id
284
+ const candidates = [
285
+ serviceName,
286
+ serviceName.replace(/[-_]service$/i, ''),
287
+ serviceName.replace(/[-_]svc$/i, ''),
288
+ ];
289
+ let row;
290
+ for (const c of candidates) {
291
+ row = db.prepare(`SELECT repo_url, repo_subpath, name FROM services WHERE name = ? OR id = ? LIMIT 1`).get(c, c);
292
+ if (row)
293
+ break;
294
+ }
295
+ if (!row) {
296
+ row = db.prepare(`SELECT repo_url, repo_subpath, name FROM services WHERE name LIKE ? LIMIT 1`).get(`%${serviceName.replace(/[-_]service$/i, '')}%`);
297
+ }
298
+ // Fallback: GPT returned a bare path without service prefix (e.g. "src/vesper_ingestion/db/repo.py")
299
+ // Try matching path segments against service names or repo_subpaths
300
+ if (!row) {
301
+ const allSvcs = db.prepare(`SELECT repo_url, repo_subpath, name FROM services WHERE is_external = 0`).all();
302
+ const fullPath = cite.file;
303
+ for (const svc of allSvcs) {
304
+ // Check if repo_subpath appears in the file path
305
+ if (svc.repo_subpath && fullPath.includes(svc.repo_subpath.split('/').pop() ?? '___')) {
306
+ row = svc;
307
+ // The gptFilePath in this case IS the full path (since no service prefix matched)
308
+ gptFilePath = fullPath;
309
+ serviceName = svc.name;
310
+ break;
311
+ }
312
+ // Check if service name appears in the file path
313
+ if (fullPath.toLowerCase().includes(svc.name.toLowerCase().replace(/-/g, '_'))) {
314
+ row = svc;
315
+ gptFilePath = fullPath;
316
+ serviceName = svc.name;
317
+ break;
318
+ }
319
+ }
320
+ }
321
+ // Fuzzy-correct the file path against real files on disk
322
+ let correctedFilePath = gptFilePath;
323
+ const serviceDir = findLocalServiceDir(row?.repo_url ?? null, row?.repo_subpath ?? null);
324
+ if (serviceDir) {
325
+ const realFiles = walkSourceFiles(serviceDir);
326
+ correctedFilePath = bestFileMatch(gptFilePath, realFiles);
327
+ }
328
+ // Build the repo-relative file path for GitHub URL
329
+ // repo_subpath is now the correct relative path (e.g. "frontends/ops-ui")
330
+ const repoRelativePath = row?.repo_subpath
331
+ ? `${row.repo_subpath}/${correctedFilePath}`
332
+ : correctedFilePath;
333
+ // GitHub URL: https://github.com/Org/Repo/blob/main/frontends/ops-ui/components/X.tsx#L45
334
+ const url = row?.repo_url && correctedFilePath
335
+ ? `${row.repo_url.replace(/\/+$/, '')}/blob/main/${repoRelativePath}#L${cite.line}`
336
+ : null;
337
+ // Update cite.file to reflect the corrected path
338
+ const correctedCiteFile = `${serviceName}/${correctedFilePath}`;
339
+ return { ...cite, file: correctedCiteFile, url };
340
+ });
341
+ const result = {
342
+ id: (0, crypto_1.randomUUID)(),
343
+ question,
344
+ answer: parsed.answer ?? 'Unable to generate answer. Please try again.',
345
+ citations,
346
+ relatedNodes: parsed.relatedNodes ?? [],
347
+ suggestions: (parsed.suggestions ?? []).slice(0, 3),
348
+ };
349
+ // Persist to history
350
+ db.prepare(`
351
+ INSERT INTO qa_sessions (id, question, answer, citations, related_nodes)
352
+ VALUES (?, ?, ?, ?, ?)
353
+ `).run(result.id, result.question, result.answer, JSON.stringify(result.citations), JSON.stringify(result.relatedNodes));
354
+ return result;
355
+ }
356
+ // ── History ───────────────────────────────────────────────────────────────────
357
+ /**
358
+ * Returns the last 20 Q&A interactions from the database.
359
+ */
360
+ function getQAHistory() {
361
+ const db = (0, index_js_1.getDb)();
362
+ const rows = db.prepare(`
363
+ SELECT id, question, answer, citations, related_nodes, created_at
364
+ FROM qa_sessions ORDER BY created_at DESC LIMIT 20
365
+ `).all();
366
+ return rows.map(r => ({
367
+ id: r.id,
368
+ question: r.question,
369
+ answer: r.answer,
370
+ citations: (() => { try {
371
+ return JSON.parse(r.citations);
372
+ }
373
+ catch {
374
+ return [];
375
+ } })(),
376
+ relatedNodes: (() => { try {
377
+ return JSON.parse(r.related_nodes);
378
+ }
379
+ catch {
380
+ return [];
381
+ } })(),
382
+ createdAt: r.created_at,
383
+ }));
384
+ }
385
+ // ── Suggestions ───────────────────────────────────────────────────────────────
386
+ /**
387
+ * Returns dynamic Q&A suggestions based on the current graph state.
388
+ * Surfaces questions relevant to unhealthy services and detected patterns.
389
+ */
390
+ function getQASuggestions() {
391
+ const db = (0, index_js_1.getDb)();
392
+ const nodes = db.prepare(`
393
+ SELECT name, health_tier, team FROM services
394
+ WHERE is_external = 0 ORDER BY health_score ASC LIMIT 5
395
+ `).all();
396
+ const suggestions = [];
397
+ for (const n of nodes) {
398
+ if (n.health_tier === 'critical') {
399
+ suggestions.push(`Why is ${n.name} in critical state?`);
400
+ }
401
+ else if (n.health_tier === 'warning') {
402
+ suggestions.push(`What's causing the warning in ${n.name}?`);
403
+ }
404
+ }
405
+ // Generic high-value questions
406
+ suggestions.push('Which service has the highest blast radius if it goes down?');
407
+ suggestions.push('What dependencies could trigger a cascade failure?');
408
+ suggestions.push('Which services are missing error handling?');
409
+ suggestions.push('What is the current overall system risk?');
410
+ // Deduplicate and limit
411
+ return [...new Set(suggestions)].slice(0, 5);
412
+ }
413
+ //# sourceMappingURL=qaService.js.map