engramx 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.
@@ -0,0 +1,1191 @@
1
+ // src/core.ts
2
+ import { join as join3, resolve as resolve2 } from "path";
3
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "fs";
4
+
5
+ // src/graph/store.ts
6
+ import initSqlJs from "sql.js";
7
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
8
+ import { dirname } from "path";
9
+ var GraphStore = class _GraphStore {
10
+ db;
11
+ dbPath;
12
+ constructor(db, dbPath) {
13
+ this.db = db;
14
+ this.dbPath = dbPath;
15
+ this.migrate();
16
+ }
17
+ static async open(dbPath) {
18
+ mkdirSync(dirname(dbPath), { recursive: true });
19
+ const SQL = await initSqlJs();
20
+ let db;
21
+ if (existsSync(dbPath)) {
22
+ const buffer = readFileSync(dbPath);
23
+ db = new SQL.Database(buffer);
24
+ } else {
25
+ db = new SQL.Database();
26
+ }
27
+ return new _GraphStore(db, dbPath);
28
+ }
29
+ migrate() {
30
+ this.db.run(`
31
+ CREATE TABLE IF NOT EXISTS nodes (
32
+ id TEXT PRIMARY KEY,
33
+ label TEXT NOT NULL,
34
+ kind TEXT NOT NULL,
35
+ source_file TEXT NOT NULL DEFAULT '',
36
+ source_location TEXT,
37
+ confidence TEXT NOT NULL DEFAULT 'EXTRACTED',
38
+ confidence_score REAL NOT NULL DEFAULT 1.0,
39
+ last_verified INTEGER NOT NULL DEFAULT 0,
40
+ query_count INTEGER NOT NULL DEFAULT 0,
41
+ metadata TEXT NOT NULL DEFAULT '{}'
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS edges (
45
+ source TEXT NOT NULL,
46
+ target TEXT NOT NULL,
47
+ relation TEXT NOT NULL,
48
+ confidence TEXT NOT NULL DEFAULT 'EXTRACTED',
49
+ confidence_score REAL NOT NULL DEFAULT 1.0,
50
+ source_file TEXT NOT NULL DEFAULT '',
51
+ source_location TEXT,
52
+ last_verified INTEGER NOT NULL DEFAULT 0,
53
+ metadata TEXT NOT NULL DEFAULT '{}',
54
+ PRIMARY KEY (source, target, relation)
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS stats (
58
+ key TEXT PRIMARY KEY,
59
+ value TEXT NOT NULL
60
+ );
61
+ `);
62
+ const indexes = [
63
+ "CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind)",
64
+ "CREATE INDEX IF NOT EXISTS idx_nodes_source_file ON nodes(source_file)",
65
+ "CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source)",
66
+ "CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target)",
67
+ "CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)"
68
+ ];
69
+ for (const sql of indexes) {
70
+ try {
71
+ this.db.run(sql);
72
+ } catch {
73
+ }
74
+ }
75
+ }
76
+ save() {
77
+ const data = this.db.export();
78
+ writeFileSync(this.dbPath, Buffer.from(data));
79
+ }
80
+ upsertNode(node) {
81
+ this.db.run(
82
+ `INSERT OR REPLACE INTO nodes (id, label, kind, source_file, source_location, confidence, confidence_score, last_verified, query_count, metadata)
83
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
84
+ [
85
+ node.id,
86
+ node.label,
87
+ node.kind,
88
+ node.sourceFile,
89
+ node.sourceLocation,
90
+ node.confidence,
91
+ node.confidenceScore,
92
+ node.lastVerified,
93
+ node.queryCount,
94
+ JSON.stringify(node.metadata)
95
+ ]
96
+ );
97
+ }
98
+ upsertEdge(edge) {
99
+ this.db.run(
100
+ `INSERT OR REPLACE INTO edges (source, target, relation, confidence, confidence_score, source_file, source_location, last_verified, metadata)
101
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
102
+ [
103
+ edge.source,
104
+ edge.target,
105
+ edge.relation,
106
+ edge.confidence,
107
+ edge.confidenceScore,
108
+ edge.sourceFile,
109
+ edge.sourceLocation,
110
+ edge.lastVerified,
111
+ JSON.stringify(edge.metadata)
112
+ ]
113
+ );
114
+ }
115
+ bulkUpsert(nodes, edges) {
116
+ this.db.run("BEGIN TRANSACTION");
117
+ for (const node of nodes) this.upsertNode(node);
118
+ for (const edge of edges) this.upsertEdge(edge);
119
+ this.db.run("COMMIT");
120
+ this.save();
121
+ }
122
+ getNode(id) {
123
+ const stmt = this.db.prepare("SELECT * FROM nodes WHERE id = ?");
124
+ stmt.bind([id]);
125
+ if (stmt.step()) {
126
+ const row = stmt.getAsObject();
127
+ stmt.free();
128
+ return this.rowToNode(row);
129
+ }
130
+ stmt.free();
131
+ return null;
132
+ }
133
+ searchNodes(query2, limit = 20) {
134
+ const pattern = `%${query2}%`;
135
+ const results = [];
136
+ const stmt = this.db.prepare(
137
+ "SELECT * FROM nodes WHERE label LIKE ? OR id LIKE ? ORDER BY query_count DESC LIMIT ?"
138
+ );
139
+ stmt.bind([pattern, pattern, limit]);
140
+ while (stmt.step()) {
141
+ results.push(this.rowToNode(stmt.getAsObject()));
142
+ }
143
+ stmt.free();
144
+ return results;
145
+ }
146
+ getNeighbors(nodeId, relationFilter) {
147
+ const sql = relationFilter ? "SELECT * FROM edges WHERE (source = ? OR target = ?) AND relation = ?" : "SELECT * FROM edges WHERE source = ? OR target = ?";
148
+ const params = relationFilter ? [nodeId, nodeId, relationFilter] : [nodeId, nodeId];
149
+ const stmt = this.db.prepare(sql);
150
+ stmt.bind(params);
151
+ const results = [];
152
+ while (stmt.step()) {
153
+ const edge = this.rowToEdge(stmt.getAsObject());
154
+ const neighborId = edge.source === nodeId ? edge.target : edge.source;
155
+ const node = this.getNode(neighborId);
156
+ if (node) results.push({ node, edge });
157
+ }
158
+ stmt.free();
159
+ return results;
160
+ }
161
+ getGodNodes(topN = 10) {
162
+ const results = [];
163
+ const stmt = this.db.prepare(
164
+ `SELECT n.*, COUNT(*) as degree
165
+ FROM nodes n
166
+ JOIN edges e ON e.source = n.id OR e.target = n.id
167
+ WHERE n.kind NOT IN ('file', 'import', 'module')
168
+ GROUP BY n.id
169
+ ORDER BY degree DESC
170
+ LIMIT ?`
171
+ );
172
+ stmt.bind([topN]);
173
+ while (stmt.step()) {
174
+ const row = stmt.getAsObject();
175
+ results.push({
176
+ node: this.rowToNode(row),
177
+ degree: row.degree
178
+ });
179
+ }
180
+ stmt.free();
181
+ return results;
182
+ }
183
+ getAllNodes() {
184
+ const results = [];
185
+ const stmt = this.db.prepare("SELECT * FROM nodes");
186
+ while (stmt.step()) {
187
+ results.push(this.rowToNode(stmt.getAsObject()));
188
+ }
189
+ stmt.free();
190
+ return results;
191
+ }
192
+ getAllEdges() {
193
+ const results = [];
194
+ const stmt = this.db.prepare("SELECT * FROM edges");
195
+ while (stmt.step()) {
196
+ results.push(this.rowToEdge(stmt.getAsObject()));
197
+ }
198
+ stmt.free();
199
+ return results;
200
+ }
201
+ incrementQueryCount(nodeId) {
202
+ this.db.run(
203
+ "UPDATE nodes SET query_count = query_count + 1 WHERE id = ?",
204
+ [nodeId]
205
+ );
206
+ }
207
+ getStats() {
208
+ const nodeCount = this.db.exec("SELECT COUNT(*) FROM nodes")[0]?.values[0]?.[0] ?? 0;
209
+ const edgeCount = this.db.exec("SELECT COUNT(*) FROM edges")[0]?.values[0]?.[0] ?? 0;
210
+ const confRows = this.db.exec(
211
+ "SELECT confidence, COUNT(*) as c FROM edges GROUP BY confidence"
212
+ );
213
+ const total = edgeCount || 1;
214
+ const confMap = {};
215
+ if (confRows[0]) {
216
+ for (const row of confRows[0].values) {
217
+ confMap[row[0]] = row[1];
218
+ }
219
+ }
220
+ const savedRow = this.db.exec(
221
+ "SELECT value FROM stats WHERE key = 'tokens_saved'"
222
+ );
223
+ const lastMinedRow = this.db.exec(
224
+ "SELECT value FROM stats WHERE key = 'last_mined'"
225
+ );
226
+ return {
227
+ nodes: nodeCount,
228
+ edges: edgeCount,
229
+ communities: 0,
230
+ extractedPct: Math.round((confMap["EXTRACTED"] ?? 0) / total * 100),
231
+ inferredPct: Math.round((confMap["INFERRED"] ?? 0) / total * 100),
232
+ ambiguousPct: Math.round((confMap["AMBIGUOUS"] ?? 0) / total * 100),
233
+ lastMined: lastMinedRow[0] ? Number(lastMinedRow[0].values[0][0]) : 0,
234
+ totalQueryTokensSaved: savedRow[0] ? Number(savedRow[0].values[0][0]) : 0
235
+ };
236
+ }
237
+ getStat(key) {
238
+ const stmt = this.db.prepare("SELECT value FROM stats WHERE key = ?");
239
+ stmt.bind([key]);
240
+ if (stmt.step()) {
241
+ const val = stmt.getAsObject().value;
242
+ stmt.free();
243
+ return val;
244
+ }
245
+ stmt.free();
246
+ return null;
247
+ }
248
+ getStatNum(key) {
249
+ const val = this.getStat(key);
250
+ return val ? Number(val) : 0;
251
+ }
252
+ setStat(key, value) {
253
+ this.db.run(
254
+ "INSERT OR REPLACE INTO stats (key, value) VALUES (?, ?)",
255
+ [key, value]
256
+ );
257
+ }
258
+ clearAll() {
259
+ this.db.run("DELETE FROM nodes");
260
+ this.db.run("DELETE FROM edges");
261
+ this.db.run("DELETE FROM stats");
262
+ }
263
+ close() {
264
+ this.save();
265
+ this.db.close();
266
+ }
267
+ rowToNode(row) {
268
+ return {
269
+ id: row.id,
270
+ label: row.label,
271
+ kind: row.kind,
272
+ sourceFile: row.source_file ?? "",
273
+ sourceLocation: row.source_location ?? null,
274
+ confidence: row.confidence ?? "EXTRACTED",
275
+ confidenceScore: row.confidence_score ?? 1,
276
+ lastVerified: row.last_verified ?? 0,
277
+ queryCount: row.query_count ?? 0,
278
+ metadata: JSON.parse(row.metadata || "{}")
279
+ };
280
+ }
281
+ rowToEdge(row) {
282
+ return {
283
+ source: row.source,
284
+ target: row.target,
285
+ relation: row.relation,
286
+ confidence: row.confidence ?? "EXTRACTED",
287
+ confidenceScore: row.confidence_score ?? 1,
288
+ sourceFile: row.source_file ?? "",
289
+ sourceLocation: row.source_location ?? null,
290
+ lastVerified: row.last_verified ?? 0,
291
+ metadata: JSON.parse(row.metadata || "{}")
292
+ };
293
+ }
294
+ };
295
+
296
+ // src/graph/query.ts
297
+ var CHARS_PER_TOKEN = 4;
298
+ function scoreNodes(store, terms) {
299
+ const allNodes = store.getAllNodes();
300
+ const scored = [];
301
+ for (const node of allNodes) {
302
+ const label = node.label.toLowerCase();
303
+ const file = node.sourceFile.toLowerCase();
304
+ let score = 0;
305
+ for (const t of terms) {
306
+ if (label.includes(t)) score += 2;
307
+ if (file.includes(t)) score += 1;
308
+ }
309
+ if (score > 0) scored.push({ score, node });
310
+ }
311
+ return scored.sort((a, b) => b.score - a.score);
312
+ }
313
+ function queryGraph(store, question, options = {}) {
314
+ const { mode = "bfs", depth = 3, tokenBudget = 2e3 } = options;
315
+ const terms = question.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
316
+ const scored = scoreNodes(store, terms);
317
+ const startNodes = scored.slice(0, 3).map((s) => s.node);
318
+ if (startNodes.length === 0) {
319
+ return { nodes: [], edges: [], text: "No matching nodes found.", estimatedTokens: 5 };
320
+ }
321
+ for (const n of startNodes) store.incrementQueryCount(n.id);
322
+ const visited = new Set(startNodes.map((n) => n.id));
323
+ const collectedEdges = [];
324
+ if (mode === "bfs") {
325
+ let frontier = new Set(startNodes.map((n) => n.id));
326
+ for (let d = 0; d < depth; d++) {
327
+ const nextFrontier = /* @__PURE__ */ new Set();
328
+ for (const nid of frontier) {
329
+ const neighbors = store.getNeighbors(nid);
330
+ for (const { node, edge } of neighbors) {
331
+ if (!visited.has(node.id)) {
332
+ nextFrontier.add(node.id);
333
+ collectedEdges.push(edge);
334
+ }
335
+ }
336
+ }
337
+ for (const id of nextFrontier) visited.add(id);
338
+ frontier = nextFrontier;
339
+ }
340
+ } else {
341
+ const stack = startNodes.map((n) => ({ id: n.id, d: 0 })).reverse();
342
+ while (stack.length > 0) {
343
+ const { id, d } = stack.pop();
344
+ if (d > depth) continue;
345
+ const neighbors = store.getNeighbors(id);
346
+ for (const { node, edge } of neighbors) {
347
+ if (!visited.has(node.id)) {
348
+ visited.add(node.id);
349
+ stack.push({ id: node.id, d: d + 1 });
350
+ collectedEdges.push(edge);
351
+ }
352
+ }
353
+ }
354
+ }
355
+ const resultNodes = [];
356
+ for (const id of visited) {
357
+ const node = store.getNode(id);
358
+ if (node) resultNodes.push(node);
359
+ }
360
+ const text = renderSubgraph(resultNodes, collectedEdges, tokenBudget);
361
+ const estimatedTokens = Math.ceil(text.length / CHARS_PER_TOKEN);
362
+ return { nodes: resultNodes, edges: collectedEdges, text, estimatedTokens };
363
+ }
364
+ function shortestPath(store, sourceTerm, targetTerm, maxHops = 8) {
365
+ const sourceTerms = sourceTerm.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
366
+ const targetTerms = targetTerm.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
367
+ const sourceScored = scoreNodes(store, sourceTerms);
368
+ const targetScored = scoreNodes(store, targetTerms);
369
+ if (sourceScored.length === 0 || targetScored.length === 0) {
370
+ return {
371
+ nodes: [],
372
+ edges: [],
373
+ text: `No nodes matching "${sourceTerm}" or "${targetTerm}".`,
374
+ estimatedTokens: 10
375
+ };
376
+ }
377
+ const srcId = sourceScored[0].node.id;
378
+ const tgtId = targetScored[0].node.id;
379
+ const queue = [[srcId]];
380
+ const seen = /* @__PURE__ */ new Set([srcId]);
381
+ while (queue.length > 0) {
382
+ const path2 = queue.shift();
383
+ const current = path2[path2.length - 1];
384
+ if (current === tgtId) {
385
+ const pathNodes = [];
386
+ const pathEdges = [];
387
+ for (let i = 0; i < path2.length; i++) {
388
+ const node = store.getNode(path2[i]);
389
+ if (node) pathNodes.push(node);
390
+ if (i < path2.length - 1) {
391
+ const neighbors2 = store.getNeighbors(path2[i]);
392
+ const edge = neighbors2.find((n) => n.node.id === path2[i + 1])?.edge;
393
+ if (edge) pathEdges.push(edge);
394
+ }
395
+ }
396
+ const text = renderPath(pathNodes, pathEdges);
397
+ return {
398
+ nodes: pathNodes,
399
+ edges: pathEdges,
400
+ text,
401
+ estimatedTokens: Math.ceil(text.length / CHARS_PER_TOKEN)
402
+ };
403
+ }
404
+ if (path2.length > maxHops) continue;
405
+ const neighbors = store.getNeighbors(current);
406
+ for (const { node } of neighbors) {
407
+ if (!seen.has(node.id)) {
408
+ seen.add(node.id);
409
+ queue.push([...path2, node.id]);
410
+ }
411
+ }
412
+ }
413
+ return {
414
+ nodes: [],
415
+ edges: [],
416
+ text: `No path found between "${sourceTerm}" and "${targetTerm}" within ${maxHops} hops.`,
417
+ estimatedTokens: 15
418
+ };
419
+ }
420
+ function renderSubgraph(nodes, edges, tokenBudget) {
421
+ const charBudget = tokenBudget * CHARS_PER_TOKEN;
422
+ const lines = [];
423
+ const degreeMap = /* @__PURE__ */ new Map();
424
+ for (const e of edges) {
425
+ degreeMap.set(e.source, (degreeMap.get(e.source) ?? 0) + 1);
426
+ degreeMap.set(e.target, (degreeMap.get(e.target) ?? 0) + 1);
427
+ }
428
+ const sorted = [...nodes].sort(
429
+ (a, b) => (degreeMap.get(b.id) ?? 0) - (degreeMap.get(a.id) ?? 0)
430
+ );
431
+ for (const n of sorted) {
432
+ lines.push(
433
+ `NODE ${n.label} [${n.kind}] src=${n.sourceFile} ${n.sourceLocation ?? ""}`
434
+ );
435
+ }
436
+ for (const e of edges) {
437
+ const srcNode = nodes.find((n) => n.id === e.source);
438
+ const tgtNode = nodes.find((n) => n.id === e.target);
439
+ if (srcNode && tgtNode) {
440
+ const conf = e.confidence === "EXTRACTED" ? "" : ` [${e.confidence} ${e.confidenceScore}]`;
441
+ lines.push(
442
+ `EDGE ${srcNode.label} --${e.relation}--> ${tgtNode.label}${conf}`
443
+ );
444
+ }
445
+ }
446
+ let output = lines.join("\n");
447
+ if (output.length > charBudget) {
448
+ output = output.slice(0, charBudget) + `
449
+ ... (truncated to ~${tokenBudget} token budget)`;
450
+ }
451
+ return output;
452
+ }
453
+ function renderPath(nodes, edges) {
454
+ if (nodes.length === 0) return "Empty path.";
455
+ const segments = [nodes[0].label];
456
+ for (let i = 0; i < edges.length; i++) {
457
+ const conf = edges[i].confidence === "EXTRACTED" ? "" : ` [${edges[i].confidence}]`;
458
+ segments.push(`--${edges[i].relation}${conf}--> ${nodes[i + 1]?.label ?? "?"}`);
459
+ }
460
+ return `Path (${edges.length} hops): ${segments.join(" ")}`;
461
+ }
462
+
463
+ // src/miners/ast-miner.ts
464
+ import { readFileSync as readFileSync2, readdirSync, realpathSync } from "fs";
465
+ import { basename, extname, join, relative } from "path";
466
+ var LANG_CONFIGS = {
467
+ typescript: {
468
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
469
+ classTypes: ["class_declaration"],
470
+ functionTypes: [
471
+ "function_declaration",
472
+ "method_definition",
473
+ "arrow_function"
474
+ ],
475
+ importTypes: ["import_statement"],
476
+ callTypes: ["call_expression"],
477
+ nameField: "name",
478
+ bodyField: "body"
479
+ },
480
+ python: {
481
+ extensions: [".py"],
482
+ classTypes: ["class_definition"],
483
+ functionTypes: ["function_definition"],
484
+ importTypes: ["import_statement", "import_from_statement"],
485
+ callTypes: ["call"],
486
+ nameField: "name",
487
+ bodyField: "body"
488
+ },
489
+ go: {
490
+ extensions: [".go"],
491
+ classTypes: ["type_declaration"],
492
+ functionTypes: ["function_declaration", "method_declaration"],
493
+ importTypes: ["import_declaration"],
494
+ callTypes: ["call_expression"],
495
+ nameField: "name",
496
+ bodyField: "body"
497
+ },
498
+ rust: {
499
+ extensions: [".rs"],
500
+ classTypes: ["struct_item", "enum_item", "trait_item"],
501
+ functionTypes: ["function_item"],
502
+ importTypes: ["use_declaration"],
503
+ callTypes: ["call_expression"],
504
+ nameField: "name",
505
+ bodyField: "body"
506
+ },
507
+ java: {
508
+ extensions: [".java"],
509
+ classTypes: ["class_declaration", "interface_declaration"],
510
+ functionTypes: ["method_declaration", "constructor_declaration"],
511
+ importTypes: ["import_declaration"],
512
+ callTypes: ["method_invocation"],
513
+ nameField: "name",
514
+ bodyField: "body"
515
+ },
516
+ ruby: {
517
+ extensions: [".rb"],
518
+ classTypes: ["class", "module"],
519
+ functionTypes: ["method"],
520
+ importTypes: ["call"],
521
+ // require/include are calls in Ruby
522
+ callTypes: ["call"],
523
+ nameField: "name",
524
+ bodyField: "body"
525
+ },
526
+ php: {
527
+ extensions: [".php"],
528
+ classTypes: ["class_declaration", "interface_declaration"],
529
+ functionTypes: ["function_definition", "method_declaration"],
530
+ importTypes: ["namespace_use_declaration"],
531
+ callTypes: ["function_call_expression"],
532
+ nameField: "name",
533
+ bodyField: "body"
534
+ }
535
+ };
536
+ var EXT_TO_LANG = {};
537
+ for (const [lang, config] of Object.entries(LANG_CONFIGS)) {
538
+ for (const ext of config.extensions) {
539
+ EXT_TO_LANG[ext] = lang;
540
+ }
541
+ }
542
+ var SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_TO_LANG));
543
+ function makeId(...parts) {
544
+ return parts.filter(Boolean).join("_").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "").toLowerCase().slice(0, 120);
545
+ }
546
+ function extractFile(filePath, rootDir) {
547
+ const ext = extname(filePath).toLowerCase();
548
+ const lang = EXT_TO_LANG[ext];
549
+ if (!lang) return { nodes: [], edges: [] };
550
+ const content = readFileSync2(filePath, "utf-8");
551
+ const lines = content.split("\n");
552
+ const relPath = relative(rootDir, filePath);
553
+ const stem = basename(filePath, ext);
554
+ const now = Date.now();
555
+ const nodes = [];
556
+ const edges = [];
557
+ const seenIds = /* @__PURE__ */ new Set();
558
+ const addNode = (id, label, kind, line) => {
559
+ if (seenIds.has(id)) return;
560
+ seenIds.add(id);
561
+ nodes.push({
562
+ id,
563
+ label,
564
+ kind,
565
+ sourceFile: relPath,
566
+ sourceLocation: line ? `L${line}` : null,
567
+ confidence: "EXTRACTED",
568
+ confidenceScore: 1,
569
+ lastVerified: now,
570
+ queryCount: 0,
571
+ metadata: { lang }
572
+ });
573
+ };
574
+ const addEdge = (source, target, relation, line) => {
575
+ edges.push({
576
+ source,
577
+ target,
578
+ relation,
579
+ confidence: "EXTRACTED",
580
+ confidenceScore: 1,
581
+ sourceFile: relPath,
582
+ sourceLocation: line ? `L${line}` : null,
583
+ lastVerified: now,
584
+ metadata: {}
585
+ });
586
+ };
587
+ const fileId = makeId(stem);
588
+ addNode(fileId, basename(filePath), "file", 1);
589
+ if (lang === "typescript" || lang === "python" || lang === "java") {
590
+ extractWithPatterns(
591
+ content,
592
+ lines,
593
+ lang,
594
+ fileId,
595
+ stem,
596
+ relPath,
597
+ addNode,
598
+ addEdge
599
+ );
600
+ } else if (lang === "go") {
601
+ extractGo(content, lines, fileId, stem, relPath, addNode, addEdge);
602
+ } else if (lang === "rust") {
603
+ extractRust(content, lines, fileId, stem, relPath, addNode, addEdge);
604
+ } else {
605
+ extractWithPatterns(
606
+ content,
607
+ lines,
608
+ lang,
609
+ fileId,
610
+ stem,
611
+ relPath,
612
+ addNode,
613
+ addEdge
614
+ );
615
+ }
616
+ return { nodes, edges };
617
+ }
618
+ function extractWithPatterns(content, lines, lang, fileId, stem, relPath, addNode, addEdge) {
619
+ const patterns = getPatterns(lang);
620
+ for (let i = 0; i < lines.length; i++) {
621
+ const line = lines[i];
622
+ const lineNum = i + 1;
623
+ for (const pat of patterns.classes) {
624
+ const match = line.match(pat);
625
+ if (match?.[1]) {
626
+ const name = match[1];
627
+ const id = makeId(stem, name);
628
+ addNode(id, name, "class", lineNum);
629
+ addEdge(fileId, id, "contains", lineNum);
630
+ }
631
+ }
632
+ for (const pat of patterns.functions) {
633
+ const match = line.match(pat);
634
+ if (match?.[1]) {
635
+ const name = match[1];
636
+ const id = makeId(stem, name);
637
+ addNode(id, `${name}()`, "function", lineNum);
638
+ addEdge(fileId, id, "contains", lineNum);
639
+ }
640
+ }
641
+ for (const pat of patterns.imports) {
642
+ const match = line.match(pat);
643
+ if (match?.[1]) {
644
+ const module = match[1].replace(/['"]/g, "").split("/").pop().replace(/\.\w+$/, "");
645
+ if (module && !module.startsWith(".")) {
646
+ const id = makeId(module);
647
+ addEdge(fileId, id, "imports", lineNum);
648
+ }
649
+ }
650
+ }
651
+ for (const pat of patterns.exports) {
652
+ const match = line.match(pat);
653
+ if (match?.[1]) {
654
+ const name = match[1];
655
+ const id = makeId(stem, name);
656
+ addEdge(fileId, id, "exports", lineNum);
657
+ }
658
+ }
659
+ }
660
+ }
661
+ function extractGo(content, lines, fileId, stem, relPath, addNode, addEdge) {
662
+ for (let i = 0; i < lines.length; i++) {
663
+ const line = lines[i];
664
+ const lineNum = i + 1;
665
+ const funcMatch = line.match(
666
+ /^func\s+(?:\([\w\s*]+\)\s+)?(\w+)\s*\(/
667
+ );
668
+ if (funcMatch?.[1]) {
669
+ const name = funcMatch[1];
670
+ const id = makeId(stem, name);
671
+ addNode(id, `${name}()`, "function", lineNum);
672
+ addEdge(fileId, id, "contains", lineNum);
673
+ }
674
+ const typeMatch = line.match(/^type\s+(\w+)\s+(struct|interface)\s*\{/);
675
+ if (typeMatch?.[1]) {
676
+ const name = typeMatch[1];
677
+ const kind = typeMatch[2] === "interface" ? "interface" : "class";
678
+ const id = makeId(stem, name);
679
+ addNode(id, name, kind, lineNum);
680
+ addEdge(fileId, id, "contains", lineNum);
681
+ }
682
+ const importMatch = line.match(/^\s*"([^"]+)"/);
683
+ if (importMatch?.[1] && i > 0 && content.includes("import")) {
684
+ const module = importMatch[1].split("/").pop();
685
+ addEdge(fileId, makeId(module), "imports", lineNum);
686
+ }
687
+ }
688
+ }
689
+ function extractRust(content, lines, fileId, stem, relPath, addNode, addEdge) {
690
+ for (let i = 0; i < lines.length; i++) {
691
+ const line = lines[i];
692
+ const lineNum = i + 1;
693
+ const fnMatch = line.match(
694
+ /^\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/
695
+ );
696
+ if (fnMatch?.[1]) {
697
+ const name = fnMatch[1];
698
+ const id = makeId(stem, name);
699
+ addNode(id, `${name}()`, "function", lineNum);
700
+ addEdge(fileId, id, "contains", lineNum);
701
+ }
702
+ const structMatch = line.match(
703
+ /^\s*(?:pub\s+)?(?:struct|enum|trait)\s+(\w+)/
704
+ );
705
+ if (structMatch?.[1]) {
706
+ const name = structMatch[1];
707
+ const id = makeId(stem, name);
708
+ addNode(id, name, "class", lineNum);
709
+ addEdge(fileId, id, "contains", lineNum);
710
+ }
711
+ const useMatch = line.match(/^\s*use\s+([\w:]+)/);
712
+ if (useMatch?.[1]) {
713
+ const parts = useMatch[1].split("::");
714
+ const module = parts[parts.length - 1];
715
+ addEdge(fileId, makeId(module), "imports", lineNum);
716
+ }
717
+ }
718
+ }
719
+ function getPatterns(lang) {
720
+ switch (lang) {
721
+ case "typescript":
722
+ return {
723
+ classes: [/^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/],
724
+ functions: [
725
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
726
+ /^\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/,
727
+ /^\s*(?:public|private|protected)\s+(?:async\s+)?(\w+)\s*\(/
728
+ ],
729
+ imports: [
730
+ /^\s*import\s+.*from\s+['"]([^'"]+)['"]/,
731
+ /^\s*import\s+['"]([^'"]+)['"]/,
732
+ /require\(\s*['"]([^'"]+)['"]\s*\)/
733
+ ],
734
+ exports: [
735
+ /^\s*export\s+(?:default\s+)?(?:class|function|const|let|var|interface|type|enum)\s+(\w+)/
736
+ ]
737
+ };
738
+ case "python":
739
+ return {
740
+ classes: [/^\s*class\s+(\w+)/],
741
+ functions: [/^\s*(?:async\s+)?def\s+(\w+)/],
742
+ imports: [
743
+ /^\s*import\s+(\w[\w.]*)/,
744
+ /^\s*from\s+(\w[\w.]*)\s+import/
745
+ ],
746
+ exports: []
747
+ // Python doesn't have explicit exports
748
+ };
749
+ case "java":
750
+ return {
751
+ classes: [
752
+ /^\s*(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+(\w+)/
753
+ ],
754
+ functions: [
755
+ /^\s*(?:public|private|protected)\s+(?:static\s+)?(?:\w+\s+)?(\w+)\s*\(/
756
+ ],
757
+ imports: [/^\s*import\s+[\w.]+\.(\w+)\s*;/],
758
+ exports: []
759
+ };
760
+ case "ruby":
761
+ return {
762
+ classes: [/^\s*(?:class|module)\s+(\w+)/],
763
+ functions: [/^\s*def\s+(\w+)/],
764
+ imports: [/^\s*require\s+['"]([^'"]+)['"]/],
765
+ exports: []
766
+ };
767
+ case "php":
768
+ return {
769
+ classes: [
770
+ /^\s*(?:abstract\s+)?(?:class|interface|trait)\s+(\w+)/
771
+ ],
772
+ functions: [/^\s*(?:public|private|protected)?\s*function\s+(\w+)/],
773
+ imports: [/^\s*use\s+([\w\\]+)/],
774
+ exports: []
775
+ };
776
+ default:
777
+ return { classes: [], functions: [], imports: [], exports: [] };
778
+ }
779
+ }
780
+ function extractDirectory(dirPath, rootDir) {
781
+ const root = rootDir ?? dirPath;
782
+ const allNodes = [];
783
+ const allEdges = [];
784
+ let fileCount = 0;
785
+ let totalLines = 0;
786
+ const visitedDirs = /* @__PURE__ */ new Set();
787
+ function walk(dir) {
788
+ let realDir;
789
+ try {
790
+ realDir = realpathSync(dir);
791
+ } catch {
792
+ return;
793
+ }
794
+ if (visitedDirs.has(realDir)) return;
795
+ visitedDirs.add(realDir);
796
+ const entries = readdirSync(dir, { withFileTypes: true });
797
+ for (const entry of entries) {
798
+ const fullPath = join(dir, entry.name);
799
+ if (entry.isDirectory()) {
800
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "__pycache__" || entry.name === "vendor" || entry.name === ".engram") {
801
+ continue;
802
+ }
803
+ walk(fullPath);
804
+ continue;
805
+ }
806
+ if (!entry.isFile()) continue;
807
+ const ext = extname(entry.name).toLowerCase();
808
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
809
+ const { nodes, edges } = extractFile(fullPath, root);
810
+ allNodes.push(...nodes);
811
+ allEdges.push(...edges);
812
+ fileCount++;
813
+ try {
814
+ const content = readFileSync2(fullPath, "utf-8");
815
+ totalLines += content.split("\n").length;
816
+ } catch {
817
+ }
818
+ }
819
+ }
820
+ walk(dirPath);
821
+ return { nodes: allNodes, edges: allEdges, fileCount, totalLines };
822
+ }
823
+
824
+ // src/miners/git-miner.ts
825
+ import { execFileSync } from "child_process";
826
+ import { resolve } from "path";
827
+ function runGit(args, cwd) {
828
+ try {
829
+ return execFileSync("git", args, {
830
+ cwd,
831
+ encoding: "utf-8",
832
+ maxBuffer: 10 * 1024 * 1024,
833
+ timeout: 15e3
834
+ }).trim();
835
+ } catch (err) {
836
+ const msg = err instanceof Error ? err.message : String(err);
837
+ if (msg.includes("TIMEOUT") || msg.includes("timed out")) {
838
+ console.error(`[engram] git command timed out: git ${args.join(" ")}`);
839
+ }
840
+ return "";
841
+ }
842
+ }
843
+ function makeId2(...parts) {
844
+ return parts.filter(Boolean).join("_").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "").toLowerCase();
845
+ }
846
+ function mineGitHistory(projectRoot, maxCommits = 200) {
847
+ const root = resolve(projectRoot);
848
+ const now = Date.now();
849
+ const nodes = [];
850
+ const edges = [];
851
+ const isGit = runGit(["rev-parse", "--git-dir"], root);
852
+ if (!isGit) return { nodes, edges };
853
+ const log = runGit(
854
+ [
855
+ "log",
856
+ `--max-count=${maxCommits}`,
857
+ "--pretty=format:%H|%an|%s",
858
+ "--name-only"
859
+ ],
860
+ root
861
+ );
862
+ if (!log) return { nodes, edges };
863
+ const coChangeMap = /* @__PURE__ */ new Map();
864
+ const fileChangeCount = /* @__PURE__ */ new Map();
865
+ const authorMap = /* @__PURE__ */ new Map();
866
+ const commitBlocks = log.split("\n\n").filter(Boolean);
867
+ for (const block of commitBlocks) {
868
+ const lines = block.split("\n").filter(Boolean);
869
+ if (lines.length === 0) continue;
870
+ const [header, ...fileLines] = lines;
871
+ const parts = header.split("|");
872
+ if (parts.length < 3) continue;
873
+ const author = parts[1];
874
+ const files = fileLines.filter(
875
+ (f) => f.length > 0 && !f.includes("|") && !f.startsWith(" ") && f.includes(".")
876
+ );
877
+ for (const file of files) {
878
+ fileChangeCount.set(file, (fileChangeCount.get(file) ?? 0) + 1);
879
+ if (!authorMap.has(file)) authorMap.set(file, /* @__PURE__ */ new Set());
880
+ authorMap.get(file).add(author);
881
+ }
882
+ for (let i = 0; i < files.length; i++) {
883
+ for (let j = i + 1; j < files.length; j++) {
884
+ const key = [files[i], files[j]].sort().join("|");
885
+ const [a, b] = key.split("|");
886
+ if (!coChangeMap.has(a)) coChangeMap.set(a, /* @__PURE__ */ new Map());
887
+ coChangeMap.get(a).set(b, (coChangeMap.get(a).get(b) ?? 0) + 1);
888
+ }
889
+ }
890
+ }
891
+ for (const [fileA, coFiles] of coChangeMap) {
892
+ for (const [fileB, count] of coFiles) {
893
+ if (count >= 3) {
894
+ const confidence = Math.min(0.95, 0.5 + count * 0.05);
895
+ const stemA = makeId2(fileA.split("/").pop()?.replace(/\.\w+$/, "") ?? fileA);
896
+ const stemB = makeId2(fileB.split("/").pop()?.replace(/\.\w+$/, "") ?? fileB);
897
+ edges.push({
898
+ source: stemA,
899
+ target: stemB,
900
+ relation: "depends_on",
901
+ confidence: "INFERRED",
902
+ confidenceScore: confidence,
903
+ sourceFile: fileA,
904
+ sourceLocation: null,
905
+ lastVerified: now,
906
+ metadata: { coChangeCount: count, miner: "git" }
907
+ });
908
+ }
909
+ }
910
+ }
911
+ const hotFiles = [...fileChangeCount.entries()].filter(([, count]) => count >= 5).sort((a, b) => b[1] - a[1]).slice(0, 20);
912
+ for (const [file, changeCount] of hotFiles) {
913
+ const stem = makeId2(file.split("/").pop()?.replace(/\.\w+$/, "") ?? file);
914
+ nodes.push({
915
+ id: `hotfile_${stem}`,
916
+ label: `\u{1F525} ${file} (${changeCount} changes)`,
917
+ kind: "pattern",
918
+ sourceFile: file,
919
+ sourceLocation: null,
920
+ confidence: "EXTRACTED",
921
+ confidenceScore: 1,
922
+ lastVerified: now,
923
+ queryCount: 0,
924
+ metadata: { changeCount, miner: "git", type: "hot_file" }
925
+ });
926
+ }
927
+ return { nodes, edges };
928
+ }
929
+
930
+ // src/miners/session-miner.ts
931
+ import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
932
+ import { join as join2, basename as basename2 } from "path";
933
+ var DECISION_PATTERNS = [
934
+ /(?:decided|chose|picked|selected|went with|using|switched to)\s+(\w[\w\s-]{2,30})\s+(?:over|instead of|rather than|because|for|since)/gi,
935
+ /(?:don't|do not|never|avoid|stop)\s+(?:use|using)\s+(\w[\w\s-]{2,30})/gi,
936
+ /(?:always|must|should)\s+(?:use|prefer)\s+(\w[\w\s-]{2,30})/gi
937
+ ];
938
+ var MISTAKE_PATTERNS = [
939
+ /(?:bug|issue|problem|error|broke|breaking|failed|crash):\s*(.{10,80})/gi,
940
+ /(?:fix|fixed|resolved|solved):\s*(.{10,80})/gi,
941
+ /(?:caused by|root cause|the issue was)\s+(.{10,80})/gi
942
+ ];
943
+ var PATTERN_PATTERNS = [
944
+ /(?:pattern|convention|approach|technique|strategy):\s*(.{10,80})/gi,
945
+ /(?:we use|our approach|the way we|standard is)\s+(.{10,60})/gi
946
+ ];
947
+ function makeId3(...parts) {
948
+ return parts.filter(Boolean).join("_").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "").toLowerCase().slice(0, 60);
949
+ }
950
+ function mineText(text, sourceFile) {
951
+ const now = Date.now();
952
+ const nodes = [];
953
+ const seenLabels = /* @__PURE__ */ new Set();
954
+ const addIfNew = (label, kind, confidence) => {
955
+ const normalized = label.trim().toLowerCase();
956
+ if (seenLabels.has(normalized) || normalized.length < 5) return;
957
+ seenLabels.add(normalized);
958
+ nodes.push({
959
+ id: makeId3("session", kind, normalized),
960
+ label: label.trim(),
961
+ kind,
962
+ sourceFile,
963
+ sourceLocation: null,
964
+ confidence: "INFERRED",
965
+ confidenceScore: confidence,
966
+ lastVerified: now,
967
+ queryCount: 0,
968
+ metadata: { miner: "session" }
969
+ });
970
+ };
971
+ for (const pattern of DECISION_PATTERNS) {
972
+ pattern.lastIndex = 0;
973
+ let match;
974
+ while ((match = pattern.exec(text)) !== null) {
975
+ if (match[1]) addIfNew(match[1], "decision", 0.7);
976
+ }
977
+ }
978
+ for (const pattern of MISTAKE_PATTERNS) {
979
+ pattern.lastIndex = 0;
980
+ let match;
981
+ while ((match = pattern.exec(text)) !== null) {
982
+ if (match[1]) addIfNew(match[1], "mistake", 0.6);
983
+ }
984
+ }
985
+ for (const pattern of PATTERN_PATTERNS) {
986
+ pattern.lastIndex = 0;
987
+ let match;
988
+ while ((match = pattern.exec(text)) !== null) {
989
+ if (match[1]) addIfNew(match[1], "pattern", 0.65);
990
+ }
991
+ }
992
+ return { nodes, edges: [] };
993
+ }
994
+ function mineSessionHistory(projectRoot) {
995
+ const allNodes = [];
996
+ const allEdges = [];
997
+ const sources = [
998
+ join2(projectRoot, "CLAUDE.md"),
999
+ join2(projectRoot, ".claude", "CLAUDE.md"),
1000
+ join2(projectRoot, "AGENTS.md"),
1001
+ join2(projectRoot, ".cursorrules"),
1002
+ join2(projectRoot, ".cursor", "rules")
1003
+ ];
1004
+ for (const source of sources) {
1005
+ if (existsSync3(source)) {
1006
+ try {
1007
+ const { nodes, edges } = mineText(readFileSync3(source, "utf-8"), basename2(source));
1008
+ allNodes.push(...nodes);
1009
+ allEdges.push(...edges);
1010
+ } catch {
1011
+ }
1012
+ }
1013
+ }
1014
+ const sessionsDir = join2(projectRoot, ".engram", "sessions");
1015
+ if (existsSync3(sessionsDir)) {
1016
+ try {
1017
+ for (const file of readdirSync2(sessionsDir).filter((f) => f.endsWith(".md"))) {
1018
+ const { nodes, edges } = mineText(
1019
+ readFileSync3(join2(sessionsDir, file), "utf-8"),
1020
+ `sessions/${file}`
1021
+ );
1022
+ allNodes.push(...nodes);
1023
+ allEdges.push(...edges);
1024
+ }
1025
+ } catch {
1026
+ }
1027
+ }
1028
+ return { nodes: allNodes, edges: allEdges };
1029
+ }
1030
+ function learnFromSession(text, sourceLabel = "session") {
1031
+ return mineText(text, sourceLabel);
1032
+ }
1033
+
1034
+ // src/core.ts
1035
+ var ENGRAM_DIR = ".engram";
1036
+ var DB_FILE = "graph.db";
1037
+ function getDbPath(projectRoot) {
1038
+ return join3(projectRoot, ENGRAM_DIR, DB_FILE);
1039
+ }
1040
+ async function getStore(projectRoot) {
1041
+ return GraphStore.open(getDbPath(projectRoot));
1042
+ }
1043
+ async function init(projectRoot) {
1044
+ const root = resolve2(projectRoot);
1045
+ const start = Date.now();
1046
+ mkdirSync2(join3(root, ENGRAM_DIR), { recursive: true });
1047
+ const { nodes, edges, fileCount, totalLines } = extractDirectory(root);
1048
+ const gitResult = mineGitHistory(root);
1049
+ const sessionResult = mineSessionHistory(root);
1050
+ const allNodes = [...nodes, ...gitResult.nodes, ...sessionResult.nodes];
1051
+ const allEdges = [...edges, ...gitResult.edges, ...sessionResult.edges];
1052
+ const store = await getStore(root);
1053
+ try {
1054
+ store.clearAll();
1055
+ store.bulkUpsert(allNodes, allEdges);
1056
+ store.setStat("last_mined", String(Date.now()));
1057
+ store.setStat("project_root", root);
1058
+ } finally {
1059
+ store.close();
1060
+ }
1061
+ return { nodes: allNodes.length, edges: allEdges.length, fileCount, totalLines, timeMs: Date.now() - start };
1062
+ }
1063
+ async function query(projectRoot, question, options = {}) {
1064
+ const store = await getStore(projectRoot);
1065
+ try {
1066
+ const result = queryGraph(store, question, options);
1067
+ return { text: result.text, estimatedTokens: result.estimatedTokens, nodesFound: result.nodes.length };
1068
+ } finally {
1069
+ store.close();
1070
+ }
1071
+ }
1072
+ async function path(projectRoot, source, target) {
1073
+ const store = await getStore(projectRoot);
1074
+ try {
1075
+ const result = shortestPath(store, source, target);
1076
+ return { text: result.text, hops: result.edges.length };
1077
+ } finally {
1078
+ store.close();
1079
+ }
1080
+ }
1081
+ async function godNodes(projectRoot, topN = 10) {
1082
+ const store = await getStore(projectRoot);
1083
+ try {
1084
+ return store.getGodNodes(topN).map((g) => ({
1085
+ label: g.node.label,
1086
+ kind: g.node.kind,
1087
+ degree: g.degree,
1088
+ sourceFile: g.node.sourceFile
1089
+ }));
1090
+ } finally {
1091
+ store.close();
1092
+ }
1093
+ }
1094
+ async function stats(projectRoot) {
1095
+ const store = await getStore(projectRoot);
1096
+ try {
1097
+ return store.getStats();
1098
+ } finally {
1099
+ store.close();
1100
+ }
1101
+ }
1102
+ async function learn(projectRoot, text, sourceLabel = "manual") {
1103
+ const { nodes, edges } = learnFromSession(text, sourceLabel);
1104
+ if (nodes.length === 0 && edges.length === 0) return { nodesAdded: 0 };
1105
+ const store = await getStore(projectRoot);
1106
+ try {
1107
+ store.bulkUpsert(nodes, edges);
1108
+ } finally {
1109
+ store.close();
1110
+ }
1111
+ return { nodesAdded: nodes.length };
1112
+ }
1113
+ async function benchmark(projectRoot, questions) {
1114
+ const root = resolve2(projectRoot);
1115
+ const store = await getStore(root);
1116
+ try {
1117
+ const allNodes = store.getAllNodes();
1118
+ let fullCorpusChars = 0;
1119
+ const seenFiles = /* @__PURE__ */ new Set();
1120
+ for (const node of allNodes) {
1121
+ if (node.sourceFile && !seenFiles.has(node.sourceFile)) {
1122
+ seenFiles.add(node.sourceFile);
1123
+ try {
1124
+ const fullPath = join3(root, node.sourceFile);
1125
+ if (existsSync4(fullPath)) fullCorpusChars += readFileSync4(fullPath, "utf-8").length;
1126
+ } catch {
1127
+ }
1128
+ }
1129
+ }
1130
+ const naiveFullCorpus = Math.ceil(fullCorpusChars / 4);
1131
+ const qs = questions ?? [
1132
+ "how does authentication work",
1133
+ "what is the main entry point",
1134
+ "how are errors handled",
1135
+ "what connects the data layer to the api",
1136
+ "what are the core abstractions"
1137
+ ];
1138
+ const perQuestion = [];
1139
+ for (const q of qs) {
1140
+ const result = queryGraph(store, q, { tokenBudget: 2e3 });
1141
+ if (result.estimatedTokens > 0) {
1142
+ const matchedFiles = new Set(result.nodes.map((n) => n.sourceFile).filter(Boolean));
1143
+ let relevantChars = 0;
1144
+ for (const f of matchedFiles) {
1145
+ try {
1146
+ const fullPath = join3(root, f);
1147
+ if (existsSync4(fullPath)) relevantChars += readFileSync4(fullPath, "utf-8").length;
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ const naiveRelevant = Math.ceil(relevantChars / 4) || 1;
1152
+ perQuestion.push({
1153
+ question: q,
1154
+ tokens: result.estimatedTokens,
1155
+ reductionFull: naiveFullCorpus > 0 ? Math.round(naiveFullCorpus / result.estimatedTokens * 10) / 10 : 0,
1156
+ reductionRelevant: Math.round(naiveRelevant / result.estimatedTokens * 10) / 10
1157
+ });
1158
+ }
1159
+ }
1160
+ const avgQueryTokens = perQuestion.length > 0 ? Math.round(perQuestion.reduce((sum, p) => sum + p.tokens, 0) / perQuestion.length) : 0;
1161
+ const avgRelevantChars = perQuestion.length > 0 ? perQuestion.reduce((sum, p) => sum + p.reductionRelevant, 0) / perQuestion.length : 0;
1162
+ return {
1163
+ naiveFullCorpus,
1164
+ naiveRelevantFiles: avgQueryTokens > 0 ? Math.round(avgQueryTokens * avgRelevantChars) : 0,
1165
+ avgQueryTokens,
1166
+ reductionVsFull: avgQueryTokens > 0 ? Math.round(naiveFullCorpus / avgQueryTokens * 10) / 10 : 0,
1167
+ reductionVsRelevant: Math.round(avgRelevantChars * 10) / 10,
1168
+ perQuestion
1169
+ };
1170
+ } finally {
1171
+ store.close();
1172
+ }
1173
+ }
1174
+
1175
+ export {
1176
+ GraphStore,
1177
+ queryGraph,
1178
+ shortestPath,
1179
+ SUPPORTED_EXTENSIONS,
1180
+ extractFile,
1181
+ extractDirectory,
1182
+ getDbPath,
1183
+ getStore,
1184
+ init,
1185
+ query,
1186
+ path,
1187
+ godNodes,
1188
+ stats,
1189
+ learn,
1190
+ benchmark
1191
+ };