graph-indexer 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -677
- package/PROMPT.md +160 -133
- package/README.md +244 -335
- package/core-engine.mjs +396 -121
- package/indexer.mjs +19 -19
- package/init.mjs +241 -0
- package/mcp-server.mjs +441 -117
- package/package.json +16 -4
- package/parser-utils.mjs +357 -17
- package/watch-daemon.mjs +25 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graph-indexer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Zero-DB, Air-gapped, In-Memory Graph Indexer MCP Server with Hybrid RRF Search and Cross-Language Topology",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "core-engine.mjs",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
".": "./core-engine.mjs"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
+
"graph-indexer": "init.mjs",
|
|
11
12
|
"idx-index": "indexer.mjs",
|
|
12
13
|
"idx-watch": "watch-daemon.mjs",
|
|
13
14
|
"idx-mcp": "mcp-server.mjs"
|
|
@@ -24,7 +25,14 @@
|
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"start": "node ./mcp-server.mjs",
|
|
27
|
-
"test": "node
|
|
28
|
+
"test": "node test/run.mjs",
|
|
29
|
+
"test:setup": "node test/setup.mjs",
|
|
30
|
+
"test:run": "node test/run.mjs",
|
|
31
|
+
"test:embeddings": "OLLAMA_HOST=http://localhost:11435 node test/run-embeddings.mjs",
|
|
32
|
+
"test:embeddings:skip": "OLLAMA_HOST=http://localhost:11435 node test/run-embeddings.mjs --skip-indexing",
|
|
33
|
+
"test:suite": "node test/run.mjs --suite",
|
|
34
|
+
"test:json": "node test/run.mjs --json",
|
|
35
|
+
"test:reset": "rm -rf test/fixtures && node test/setup.mjs",
|
|
28
36
|
"mcp:index": "node indexer.mjs --repo .",
|
|
29
37
|
"mcp:watch": "node watch-daemon.mjs",
|
|
30
38
|
"mcp:start": "node mcp-server.mjs"
|
|
@@ -34,11 +42,15 @@
|
|
|
34
42
|
"chokidar": "5.0.0",
|
|
35
43
|
"ignore": "5.3.2",
|
|
36
44
|
"tree-sitter": "0.21.1",
|
|
45
|
+
"tree-sitter-c-sharp": "^0.21.3",
|
|
37
46
|
"tree-sitter-css": "0.21.1",
|
|
38
47
|
"tree-sitter-go": "0.21.0",
|
|
48
|
+
"tree-sitter-java": "^0.21.0",
|
|
39
49
|
"tree-sitter-javascript": "0.21.4",
|
|
50
|
+
"tree-sitter-kotlin": "^0.3.8",
|
|
40
51
|
"tree-sitter-php": "0.23.1",
|
|
41
52
|
"tree-sitter-python": "0.21.0",
|
|
53
|
+
"tree-sitter-ruby": "^0.23.1",
|
|
42
54
|
"tree-sitter-rust": "0.21.0",
|
|
43
55
|
"tree-sitter-typescript": "0.21.2",
|
|
44
56
|
"zod": "3.23.8"
|
|
@@ -60,7 +72,7 @@
|
|
|
60
72
|
"url": "https://github.com/MaquinaTech",
|
|
61
73
|
"email": "nicolopezdelerma@gmail.com"
|
|
62
74
|
},
|
|
63
|
-
"license": "
|
|
75
|
+
"license": "MIT",
|
|
64
76
|
"engines": {
|
|
65
77
|
"node": ">=18.0.0"
|
|
66
78
|
},
|
|
@@ -78,4 +90,4 @@
|
|
|
78
90
|
"optionalDependencies": {
|
|
79
91
|
"hnswlib-node": "^3.0.0"
|
|
80
92
|
}
|
|
81
|
-
}
|
|
93
|
+
}
|
package/parser-utils.mjs
CHANGED
|
@@ -3,23 +3,23 @@
|
|
|
3
3
|
* @description Shared AST parsing, language registry, and embedding utilities.
|
|
4
4
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
5
5
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
6
|
-
* @license
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* @license MIT
|
|
7
|
+
* Copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions: The above copyright
|
|
14
|
+
* notice and this permission notice shall be included in all copies or
|
|
15
|
+
* substantial portions of the Software.
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import { createHash } from 'crypto';
|
|
20
20
|
import ignore from 'ignore';
|
|
21
21
|
import Parser from 'tree-sitter';
|
|
22
|
-
const { Query } = Parser; //
|
|
22
|
+
const { Query } = Parser; // Native parser query helper
|
|
23
23
|
|
|
24
24
|
import TypeScript from 'tree-sitter-typescript';
|
|
25
25
|
import JavaScript from 'tree-sitter-javascript';
|
|
@@ -28,6 +28,10 @@ import Python from 'tree-sitter-python';
|
|
|
28
28
|
import Rust from 'tree-sitter-rust';
|
|
29
29
|
import Go from 'tree-sitter-go';
|
|
30
30
|
import PHP from 'tree-sitter-php';
|
|
31
|
+
import Java from 'tree-sitter-java';
|
|
32
|
+
import Kotlin from 'tree-sitter-kotlin';
|
|
33
|
+
import CSharp from 'tree-sitter-c-sharp';
|
|
34
|
+
import Ruby from 'tree-sitter-ruby';
|
|
31
35
|
import { truncateForEmbedding } from './core-engine.mjs';
|
|
32
36
|
|
|
33
37
|
export const MAX_FILE_SIZE_BYTES = 500000;
|
|
@@ -39,6 +43,10 @@ const LANGUAGE_MAP = {
|
|
|
39
43
|
'.css': CSS, '.scss': CSS,
|
|
40
44
|
'.py': Python, '.rs': Rust, '.go': Go,
|
|
41
45
|
'.php': PHP.php,
|
|
46
|
+
'.java': Java,
|
|
47
|
+
'.kt': Kotlin, '.kts': Kotlin,
|
|
48
|
+
'.cs': CSharp,
|
|
49
|
+
'.rb': Ruby,
|
|
42
50
|
};
|
|
43
51
|
|
|
44
52
|
// 🥇 ULTRA-GENERIC QUERIES: Immune to grammar changes between TS/JS/TSX
|
|
@@ -73,6 +81,34 @@ const LANGUAGE_QUERIES = {
|
|
|
73
81
|
(function_definition) @chunk
|
|
74
82
|
(class_declaration) @chunk
|
|
75
83
|
(expression_statement) @chunk
|
|
84
|
+
`,
|
|
85
|
+
java: `
|
|
86
|
+
(method_declaration) @chunk
|
|
87
|
+
(class_declaration) @chunk
|
|
88
|
+
(interface_declaration) @chunk
|
|
89
|
+
(constructor_declaration) @chunk
|
|
90
|
+
(enum_declaration) @chunk
|
|
91
|
+
`,
|
|
92
|
+
kotlin: `
|
|
93
|
+
(function_declaration) @chunk
|
|
94
|
+
(class_declaration) @chunk
|
|
95
|
+
(object_declaration) @chunk
|
|
96
|
+
(companion_object) @chunk
|
|
97
|
+
(secondary_constructor) @chunk
|
|
98
|
+
`,
|
|
99
|
+
cs: `
|
|
100
|
+
(method_declaration) @chunk
|
|
101
|
+
(class_declaration) @chunk
|
|
102
|
+
(interface_declaration) @chunk
|
|
103
|
+
(constructor_declaration) @chunk
|
|
104
|
+
(enum_declaration) @chunk
|
|
105
|
+
(property_declaration) @chunk
|
|
106
|
+
`,
|
|
107
|
+
rb: `
|
|
108
|
+
(method) @chunk
|
|
109
|
+
(singleton_method) @chunk
|
|
110
|
+
(class) @chunk
|
|
111
|
+
(module) @chunk
|
|
76
112
|
`
|
|
77
113
|
};
|
|
78
114
|
|
|
@@ -84,7 +120,15 @@ const CONTAINERS = new Set([
|
|
|
84
120
|
'lexical_declaration', 'expression_statement', 'export_statement',
|
|
85
121
|
'function_definition', 'class_definition', 'rule_set',
|
|
86
122
|
'function_item', 'struct_item', 'enum_item', 'trait_item', 'impl_item',
|
|
87
|
-
'method_declaration', 'type_declaration'
|
|
123
|
+
'method_declaration', 'type_declaration',
|
|
124
|
+
// Java / C#
|
|
125
|
+
'interface_declaration', 'constructor_declaration', 'enum_declaration',
|
|
126
|
+
// Kotlin
|
|
127
|
+
'object_declaration', 'companion_object', 'secondary_constructor',
|
|
128
|
+
// C#
|
|
129
|
+
'property_declaration',
|
|
130
|
+
// Ruby
|
|
131
|
+
'method', 'singleton_method', 'module'
|
|
88
132
|
]);
|
|
89
133
|
|
|
90
134
|
export function getParserForFile(ext) {
|
|
@@ -183,6 +227,41 @@ export function extractImportsFromAST(rootNode, ext) {
|
|
|
183
227
|
c.type === 'string' || c.type === 'encapsed_string');
|
|
184
228
|
if (strNode) imports.add(strNode.text.replace(/['"]/g, ''));
|
|
185
229
|
}
|
|
230
|
+
} // ── Java ─────────────────────────────────────────────────────────────
|
|
231
|
+
else if (ext === '.java') {
|
|
232
|
+
if (node.type === 'import_declaration') {
|
|
233
|
+
const scopedId = node.children.find(c => c.type === 'scoped_identifier' || c.type === 'identifier');
|
|
234
|
+
if (scopedId) imports.add(scopedId.text.replace(/\./g, '/'));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ── Kotlin ───────────────────────────────────────────────────────────
|
|
238
|
+
else if (ext === '.kt' || ext === '.kts') {
|
|
239
|
+
if (node.type === 'import_header') {
|
|
240
|
+
const path = node.children.find(c => c.type === 'identifier' || c.type === 'user_type' || c.isNamed);
|
|
241
|
+
const raw = node.text.replace(/^import\s+/, '').replace(/\s*\.\*\s*$/, '').trim();
|
|
242
|
+
if (raw) imports.add(raw.replace(/\./g, '/'));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ── C# ───────────────────────────────────────────────────────────────
|
|
246
|
+
else if (ext === '.cs') {
|
|
247
|
+
if (node.type === 'using_directive') {
|
|
248
|
+
const ns = node.children.find(c => c.type === 'qualified_name' || c.type === 'identifier' || c.type === 'name_equals');
|
|
249
|
+
if (ns) {
|
|
250
|
+
const raw = ns.text.replace(/\s*=\s*.*$/, '').trim();
|
|
251
|
+
if (raw) imports.add(raw.replace(/\./g, '/'));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// ── Ruby ──────────────────────────────────────────────────────────────
|
|
256
|
+
else if (ext === '.rb') {
|
|
257
|
+
if (node.type === 'call' || node.type === 'method_call') {
|
|
258
|
+
const method = node.childForFieldName?.('method') || node.children[0];
|
|
259
|
+
if (method && (method.text === 'require' || method.text === 'require_relative')) {
|
|
260
|
+
const args = node.childForFieldName?.('arguments') || node.children.find(c => c.type === 'argument_list');
|
|
261
|
+
const strArg = args?.children?.find(c => c.type === 'string' || c.type === 'simple_string');
|
|
262
|
+
if (strArg) imports.add(strArg.text.replace(/['"]/g, ''));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
186
265
|
}
|
|
187
266
|
node.children.forEach(walk);
|
|
188
267
|
}
|
|
@@ -196,7 +275,12 @@ export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
|
196
275
|
if (!parser) return chunks;
|
|
197
276
|
|
|
198
277
|
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
199
|
-
const
|
|
278
|
+
const EXT_TO_LANG = {
|
|
279
|
+
'.java': 'java', '.kt': 'kotlin', '.kts': 'kotlin', '.cs': 'cs',
|
|
280
|
+
'.rb': 'rb'
|
|
281
|
+
};
|
|
282
|
+
const langKey = JS_LIKE.includes(ext) ? 'ts'
|
|
283
|
+
: (EXT_TO_LANG[ext] || (LANGUAGE_QUERIES[ext.slice(1)] ? ext.slice(1) : null));
|
|
200
284
|
if (!langKey || !LANGUAGE_QUERIES[langKey]) return chunks;
|
|
201
285
|
|
|
202
286
|
// 🥇 HEADER INHERITANCE: Extract top-of-file/global comments for module context
|
|
@@ -226,10 +310,14 @@ export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
|
226
310
|
// Filter out very small fragments (simple variables, etc.)
|
|
227
311
|
if (chunkNode.endPosition.row - chunkNode.startPosition.row < 2) continue;
|
|
228
312
|
|
|
229
|
-
// 🥇 DEDUPLICATION LOGIC: Ignore nodes that are nested inside other container nodes
|
|
313
|
+
// 🥇 DEDUPLICATION LOGIC: Ignore nodes that are nested inside other container nodes.
|
|
314
|
+
// Stop at the actual tree root (parent === null) to be language-agnostic:
|
|
315
|
+
// Python root = 'module', JS root = 'program', Go root = 'source_file', Ruby root = 'program'.
|
|
316
|
+
// Stopping at 'program' alone was falsely marking top-level Python classes as nested
|
|
317
|
+
// because Ruby's 'module' keyword (also in CONTAINERS) shares the name with Python's root.
|
|
230
318
|
let isNested = false;
|
|
231
319
|
let currentParent = chunkNode.parent;
|
|
232
|
-
while (currentParent && currentParent.
|
|
320
|
+
while (currentParent && currentParent.parent !== null) {
|
|
233
321
|
if (CONTAINERS.has(currentParent.type)) {
|
|
234
322
|
isNested = true;
|
|
235
323
|
break;
|
|
@@ -306,6 +394,12 @@ export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
|
306
394
|
const hash = generateChunkHash(docstring + snippet);
|
|
307
395
|
const outgoingCalls = extractCalls(chunkNode);
|
|
308
396
|
|
|
397
|
+
// 🥇 PARAMETER / TYPE / CLASS CONTEXT ENRICHMENT (improves recall on undocumented code)
|
|
398
|
+
const params = extractParams(chunkNode, ext);
|
|
399
|
+
const returnType = extractReturnType(chunkNode, ext);
|
|
400
|
+
const classContext = extractClassContext(chunkNode);
|
|
401
|
+
const typeRefs = extractTypeAnnotations(chunkNode, ext);
|
|
402
|
+
|
|
309
403
|
const id = createHash('sha256')
|
|
310
404
|
.update(`${relPath}::${chunkNode.startPosition.row}::${chunkNode.startPosition.column}`)
|
|
311
405
|
.digest('hex').slice(0, 24);
|
|
@@ -314,7 +408,9 @@ export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
|
314
408
|
id, file_path: relPath, node_type: chunkNode.type, name: nameText,
|
|
315
409
|
docstring: docstring, code_snippet: snippet, content_hash: hash,
|
|
316
410
|
start_line: chunkNode.startPosition.row + 1, end_line: chunkNode.endPosition.row + 1,
|
|
317
|
-
calls: outgoingCalls
|
|
411
|
+
calls: outgoingCalls,
|
|
412
|
+
params, return_type: returnType, class_context: classContext,
|
|
413
|
+
type_refs: typeRefs,
|
|
318
414
|
});
|
|
319
415
|
}
|
|
320
416
|
} catch (e) {
|
|
@@ -324,6 +420,104 @@ export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
|
324
420
|
return chunks;
|
|
325
421
|
}
|
|
326
422
|
|
|
423
|
+
// ─── Barrel export resolution ─────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
// Module-level cache: barrelAbsPath → Map<exportedName, sourceRelPath>
|
|
426
|
+
const _barrelCache = new Map();
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Parses a barrel file (index.ts / index.js) and returns a map of
|
|
430
|
+
* exportedName → sourceFilePath (relative to projectRoot).
|
|
431
|
+
* e.g. { useAuthStore: 'src/stores/authStore.ts' }
|
|
432
|
+
*/
|
|
433
|
+
export function resolveBarrelExports(barrelAbsPath, projectRoot) {
|
|
434
|
+
if (_barrelCache.has(barrelAbsPath)) return _barrelCache.get(barrelAbsPath);
|
|
435
|
+
|
|
436
|
+
const result = new Map();
|
|
437
|
+
_barrelCache.set(barrelAbsPath, result);
|
|
438
|
+
|
|
439
|
+
const ext = path.extname(barrelAbsPath);
|
|
440
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) return result;
|
|
441
|
+
|
|
442
|
+
let content;
|
|
443
|
+
try { content = fs.readFileSync(barrelAbsPath, 'utf-8'); } catch { return result; }
|
|
444
|
+
|
|
445
|
+
const parser = getParserForFile(ext);
|
|
446
|
+
if (!parser) return result;
|
|
447
|
+
|
|
448
|
+
let tree;
|
|
449
|
+
try { tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null); } catch { return result; }
|
|
450
|
+
|
|
451
|
+
const barrelDir = path.dirname(barrelAbsPath);
|
|
452
|
+
|
|
453
|
+
function walk(node) {
|
|
454
|
+
// export { X, Y as Z } from './source'
|
|
455
|
+
if (node.type === 'export_statement') {
|
|
456
|
+
const fromNode = node.children.find(c => c.type === 'string');
|
|
457
|
+
if (!fromNode) { node.children.forEach(walk); return; }
|
|
458
|
+
|
|
459
|
+
const rawSource = fromNode.text.replace(/['"]/g, '');
|
|
460
|
+
if (!rawSource.startsWith('.')) { node.children.forEach(walk); return; }
|
|
461
|
+
|
|
462
|
+
// Resolve the source file
|
|
463
|
+
const absSource = path.resolve(barrelDir, rawSource);
|
|
464
|
+
let finalAbs = null;
|
|
465
|
+
if (EXTENSIONS.has(path.extname(absSource)) && fs.existsSync(absSource)) {
|
|
466
|
+
finalAbs = absSource;
|
|
467
|
+
} else {
|
|
468
|
+
for (const e of EXTENSIONS) {
|
|
469
|
+
if (fs.existsSync(absSource + e)) { finalAbs = absSource + e; break; }
|
|
470
|
+
const idx = path.join(absSource, 'index' + e);
|
|
471
|
+
if (fs.existsSync(idx)) { finalAbs = idx; break; }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (!finalAbs) { node.children.forEach(walk); return; }
|
|
475
|
+
|
|
476
|
+
const relSource = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
|
|
477
|
+
|
|
478
|
+
// Walk named exports
|
|
479
|
+
const namedExports = node.children.find(c => c.type === 'named_imports' || c.type === 'export_clause');
|
|
480
|
+
if (namedExports) {
|
|
481
|
+
for (const child of namedExports.children) {
|
|
482
|
+
if (child.type === 'import_specifier' || child.type === 'export_specifier') {
|
|
483
|
+
// `alias as exported` or just `name`
|
|
484
|
+
const names = child.children.filter(c => c.type === 'identifier');
|
|
485
|
+
if (names.length > 0) {
|
|
486
|
+
// The exported name is the last identifier (the alias if present)
|
|
487
|
+
result.set(names[names.length - 1].text, relSource);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// export * from './source' → map the source file itself
|
|
494
|
+
const starNode = node.children.find(c => c.text === '*');
|
|
495
|
+
if (starNode) {
|
|
496
|
+
result.set('*', relSource);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
node.children.forEach(walk);
|
|
500
|
+
}
|
|
501
|
+
walk(tree.rootNode);
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ─── Go module-name cache (reads go.mod once per project root) ──────────────
|
|
506
|
+
const _goModCache = new Map();
|
|
507
|
+
function _readGoModuleName(projectRoot) {
|
|
508
|
+
if (_goModCache.has(projectRoot)) return _goModCache.get(projectRoot);
|
|
509
|
+
const modFile = path.join(projectRoot, 'go.mod');
|
|
510
|
+
let name = null;
|
|
511
|
+
if (fs.existsSync(modFile)) {
|
|
512
|
+
try {
|
|
513
|
+
const first = fs.readFileSync(modFile, 'utf-8').split('\n').find(l => l.trimStart().startsWith('module '));
|
|
514
|
+
if (first) name = first.trim().replace(/^module\s+/, '').split(/\s/)[0];
|
|
515
|
+
} catch { /* ignore */ }
|
|
516
|
+
}
|
|
517
|
+
_goModCache.set(projectRoot, name);
|
|
518
|
+
return name;
|
|
519
|
+
}
|
|
520
|
+
|
|
327
521
|
export function resolveLocalImports(rawImports, fromFileRelPath, projectRoot) {
|
|
328
522
|
const fileDir = path.dirname(path.join(projectRoot, fromFileRelPath));
|
|
329
523
|
const ext = path.extname(fromFileRelPath);
|
|
@@ -348,7 +542,46 @@ export function resolveLocalImports(rawImports, fromFileRelPath, projectRoot) {
|
|
|
348
542
|
}
|
|
349
543
|
}
|
|
350
544
|
}
|
|
351
|
-
if (finalAbs)
|
|
545
|
+
if (finalAbs) {
|
|
546
|
+
const relPath = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
|
|
547
|
+
const baseName = path.basename(finalAbs, path.extname(finalAbs));
|
|
548
|
+
// 🥇 BARREL RESOLUTION: If the resolved file is an index file, expand barrel exports
|
|
549
|
+
if (baseName === 'index' && ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(path.extname(finalAbs))) {
|
|
550
|
+
const barrelMap = resolveBarrelExports(finalAbs, projectRoot);
|
|
551
|
+
if (barrelMap.size > 0) {
|
|
552
|
+
// Add all unique source files referenced by this barrel
|
|
553
|
+
const sources = new Set(barrelMap.values());
|
|
554
|
+
for (const src of sources) {
|
|
555
|
+
if (!resolved.includes(src)) resolved.push(src);
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
// Barrel has no re-exports — keep the barrel file itself
|
|
559
|
+
if (!resolved.includes(relPath)) resolved.push(relPath);
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
if (!resolved.includes(relPath)) resolved.push(relPath);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// ── Go intra-module: github.com/owner/repo/sub → sub/*.go ──────────
|
|
567
|
+
// Go import paths use the module name as prefix; map them to local dirs.
|
|
568
|
+
else if (ext === '.go') {
|
|
569
|
+
const modName = _readGoModuleName(projectRoot);
|
|
570
|
+
if (modName && raw.startsWith(modName + '/')) {
|
|
571
|
+
const subPkg = raw.slice(modName.length + 1); // e.g. 'render'
|
|
572
|
+
const absDir = path.join(projectRoot, subPkg);
|
|
573
|
+
if (fs.existsSync(absDir)) {
|
|
574
|
+
try {
|
|
575
|
+
const goFiles = fs.readdirSync(absDir)
|
|
576
|
+
.filter(f => f.endsWith('.go') && !f.includes('_test'))
|
|
577
|
+
.slice(0, 5); // cap: take a representative sample
|
|
578
|
+
for (const gof of goFiles) {
|
|
579
|
+
const rel = path.relative(projectRoot, path.join(absDir, gof)).replace(/\\/g, '/');
|
|
580
|
+
if (!resolved.includes(rel)) resolved.push(rel);
|
|
581
|
+
}
|
|
582
|
+
} catch { /* directory unreadable — skip */ }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
352
585
|
}
|
|
353
586
|
// ── Rust crate-local: crate::module::item → src/module.rs ──────────
|
|
354
587
|
else if (ext === '.rs' && raw.startsWith('crate::')) {
|
|
@@ -419,6 +652,103 @@ export async function getLocalEmbeddingsBatch(texts, graceful = true) {
|
|
|
419
652
|
return null;
|
|
420
653
|
}
|
|
421
654
|
|
|
655
|
+
// ─── Enrichment helpers (param names, return type, class context) ─────────────
|
|
656
|
+
|
|
657
|
+
export function extractParams(chunkNode, ext) {
|
|
658
|
+
const params = [];
|
|
659
|
+
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
660
|
+
const paramTypes = JS_LIKE.includes(ext)
|
|
661
|
+
? ['required_parameter', 'optional_parameter', 'formal_parameters', 'identifier']
|
|
662
|
+
: ['parameter', 'formal_parameter', 'identifier'];
|
|
663
|
+
|
|
664
|
+
function walkParams(node) {
|
|
665
|
+
if (node.type === 'formal_parameters' || node.type === 'parameters' || node.type === 'parameter_list') {
|
|
666
|
+
for (const child of node.children) {
|
|
667
|
+
// TS: required_parameter / optional_parameter have an identifier child
|
|
668
|
+
if (child.type === 'required_parameter' || child.type === 'optional_parameter' || child.type === 'formal_parameter') {
|
|
669
|
+
const id = child.childForFieldName?.('pattern') || child.childForFieldName?.('name') ||
|
|
670
|
+
child.children.find(c => c.type === 'identifier');
|
|
671
|
+
if (id) params.push(id.text);
|
|
672
|
+
// also grab type annotation text
|
|
673
|
+
const typeAnnotation = child.childForFieldName?.('type');
|
|
674
|
+
if (typeAnnotation) {
|
|
675
|
+
const typeText = typeAnnotation.text.replace(/^:\s*/, '').trim();
|
|
676
|
+
if (typeText) params.push(typeText);
|
|
677
|
+
}
|
|
678
|
+
} else if (child.type === 'identifier') {
|
|
679
|
+
params.push(child.text);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
for (const child of node.children) walkParams(child);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Only walk the direct params node to avoid deep recursion into body
|
|
687
|
+
const paramsNode = chunkNode.childForFieldName?.('parameters') || chunkNode.childForFieldName?.('formal_parameters');
|
|
688
|
+
if (paramsNode) walkParams(paramsNode);
|
|
689
|
+
return [...new Set(params)].filter(p => p && p.length > 1).slice(0, 15);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function extractReturnType(chunkNode, ext) {
|
|
693
|
+
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
694
|
+
if (JS_LIKE.includes(ext)) {
|
|
695
|
+
const retTypeNode = chunkNode.childForFieldName?.('return_type');
|
|
696
|
+
if (retTypeNode) return retTypeNode.text.replace(/^:\s*/, '').trim().slice(0, 80);
|
|
697
|
+
}
|
|
698
|
+
return '';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function extractClassContext(chunkNode) {
|
|
702
|
+
let parent = chunkNode.parent;
|
|
703
|
+
while (parent) {
|
|
704
|
+
if (parent.type === 'class_declaration' || parent.type === 'class_definition' ||
|
|
705
|
+
parent.type === 'class_body' || parent.type === 'impl_item') {
|
|
706
|
+
const nameNode = parent.childForFieldName?.('name');
|
|
707
|
+
if (nameNode) return nameNode.text;
|
|
708
|
+
}
|
|
709
|
+
parent = parent.parent;
|
|
710
|
+
}
|
|
711
|
+
return '';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Frontier 2: Extract TypeScript/Python type annotation names from a chunk node.
|
|
716
|
+
* Returns simple type names (e.g. ['User', 'AuthToken', 'PaymentService'])
|
|
717
|
+
* used to enrich the inverted index and the type_refs chunk field.
|
|
718
|
+
*/
|
|
719
|
+
export function extractTypeAnnotations(chunkNode, ext) {
|
|
720
|
+
const types = new Set();
|
|
721
|
+
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
722
|
+
if (!JS_LIKE.includes(ext) && ext !== '.py') return [];
|
|
723
|
+
|
|
724
|
+
function walk(node) {
|
|
725
|
+
// TypeScript: type_annotation nodes contain the type text
|
|
726
|
+
if (node.type === 'type_annotation') {
|
|
727
|
+
const typeText = node.text.replace(/^:\s*/, '').trim();
|
|
728
|
+
// Extract simple identifiers from the type (skip primitives)
|
|
729
|
+
const PRIMITIVES = new Set(['string','number','boolean','void','any','unknown','never','null','undefined','object','symbol','bigint']);
|
|
730
|
+
for (const match of typeText.matchAll(/\b([A-Z][A-Za-z0-9]*)\b/g)) {
|
|
731
|
+
if (!PRIMITIVES.has(match[1].toLowerCase())) types.add(match[1]);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// TypeScript: generic_type, predefined_type (string, number…) skip, named types keep
|
|
735
|
+
else if (node.type === 'type_identifier' || node.type === 'generic_type') {
|
|
736
|
+
const name = node.children[0]?.text || node.text;
|
|
737
|
+
if (name && /^[A-Z]/.test(name)) types.add(name);
|
|
738
|
+
}
|
|
739
|
+
// Python: type comments or annotations (annotation nodes)
|
|
740
|
+
else if (node.type === 'annotation' && ext === '.py') {
|
|
741
|
+
const typeText = node.text.replace(/^->\s*|^:\s*/, '').trim();
|
|
742
|
+
for (const match of typeText.matchAll(/\b([A-Z][A-Za-z0-9]*)\b/g)) {
|
|
743
|
+
types.add(match[1]);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const child of node.children) walk(child);
|
|
747
|
+
}
|
|
748
|
+
walk(chunkNode);
|
|
749
|
+
return Array.from(types).slice(0, 20);
|
|
750
|
+
}
|
|
751
|
+
|
|
422
752
|
export function extractCalls(rootNode) {
|
|
423
753
|
const calls = new Set();
|
|
424
754
|
function walk(node) {
|
|
@@ -450,6 +780,16 @@ export function extractCalls(rootNode) {
|
|
|
450
780
|
const macroNode = node.childForFieldName?.('macro') || node.children[0];
|
|
451
781
|
if (macroNode && macroNode.type === 'identifier') calls.add(macroNode.text + '!');
|
|
452
782
|
}
|
|
783
|
+
// Java / C#: method_invocation
|
|
784
|
+
else if (node.type === 'method_invocation') {
|
|
785
|
+
const nameNode = node.childForFieldName?.('name') || node.children.find(c => c.type === 'identifier');
|
|
786
|
+
if (nameNode) calls.add(nameNode.text);
|
|
787
|
+
}
|
|
788
|
+
// Ruby: method_call / call
|
|
789
|
+
else if (node.type === 'method_call') {
|
|
790
|
+
const method = node.childForFieldName?.('method') || node.children.find(c => c.type === 'identifier');
|
|
791
|
+
if (method && method.type === 'identifier') calls.add(method.text);
|
|
792
|
+
}
|
|
453
793
|
node.children.forEach(walk);
|
|
454
794
|
}
|
|
455
795
|
walk(rootNode);
|
package/watch-daemon.mjs
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
* @description Native FileSystem Watcher Daemon to maintain an in-memory graph index. Utilizes Tree-sitter for AST parsing and local embedding generation.
|
|
5
5
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
6
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
|
-
* @license
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* @license MIT
|
|
8
|
+
* Copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions: The above copyright
|
|
15
|
+
* notice and this permission notice shall be included in all copies or
|
|
16
|
+
* substantial portions of the Software.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import fs from 'fs';
|
|
@@ -62,7 +62,7 @@ async function processFileChange(absolutePath) {
|
|
|
62
62
|
const parser = getParserForFile(ext);
|
|
63
63
|
if (!parser) return;
|
|
64
64
|
|
|
65
|
-
const tree = parser.parse(content);
|
|
65
|
+
const tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null);
|
|
66
66
|
const imports = resolveLocalImports(extractImportsFromAST(tree.rootNode, ext), filename, PROJECT_ROOT);
|
|
67
67
|
db.updateFileGraph(filename, imports);
|
|
68
68
|
|
|
@@ -114,20 +114,29 @@ async function processFileChange(absolutePath) {
|
|
|
114
114
|
|
|
115
115
|
// Unified lexical re-indexing of neighborhood
|
|
116
116
|
const cleanDeps = imports.map(d => d.split('/').pop().split('.')[0]);
|
|
117
|
-
const enrichedContext =
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const enrichedContext = [
|
|
118
|
+
chunk.name,
|
|
119
|
+
chunk.docstring || '',
|
|
120
|
+
cleanDeps.join(' '),
|
|
121
|
+
(chunk.calls || []).join(' '),
|
|
122
|
+
(chunk.params || []).join(' '),
|
|
123
|
+
chunk.return_type || '',
|
|
124
|
+
chunk.class_context ? `${chunk.class_context}.${chunk.name}` : '',
|
|
125
|
+
chunk.code_snippet
|
|
126
|
+
].join(' ');
|
|
127
|
+
|
|
128
|
+
db._indexLexical(chunk.id, enrichedContext, chunk.file_path);
|
|
120
129
|
db.chunks.set(chunk.id, chunk);
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
db.saveDebounced();
|
|
124
133
|
process.stderr.write(`[daemon] 🔄 Synced: ${filename} (Cache Hit: ${newChunks.length - chunksToEmbed.length}/${newChunks.length})\n`);
|
|
125
134
|
} catch (err) {
|
|
126
|
-
process.stderr.write(`[daemon] ❌ Error
|
|
135
|
+
process.stderr.write(`[daemon] ❌ Error in ${filename}: ${err.message}\n`);
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
process.stderr.write(`🚀 Chokidar Watcher Daemon
|
|
139
|
+
process.stderr.write(`🚀 Chokidar Watcher Daemon started in: ${PROJECT_ROOT}\n`);
|
|
131
140
|
const watcher = chokidar.watch(PROJECT_ROOT, {
|
|
132
141
|
ignored: /(^|[\/\\])\../, persistent: true,
|
|
133
142
|
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
|