graph-indexer 1.0.0 → 1.0.1
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 +35 -191
- package/README.md +794 -242
- package/core-engine.mjs +91 -20
- package/indexer.mjs +11 -11
- package/init.mjs +241 -0
- package/mcp-server.mjs +90 -13
- package/package.json +16 -4
- package/parser-utils.mjs +316 -16
- package/watch-daemon.mjs +23 -14
package/parser-utils.mjs
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
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';
|
|
@@ -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,11 @@ 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
|
+
|
|
309
402
|
const id = createHash('sha256')
|
|
310
403
|
.update(`${relPath}::${chunkNode.startPosition.row}::${chunkNode.startPosition.column}`)
|
|
311
404
|
.digest('hex').slice(0, 24);
|
|
@@ -314,7 +407,8 @@ export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
|
314
407
|
id, file_path: relPath, node_type: chunkNode.type, name: nameText,
|
|
315
408
|
docstring: docstring, code_snippet: snippet, content_hash: hash,
|
|
316
409
|
start_line: chunkNode.startPosition.row + 1, end_line: chunkNode.endPosition.row + 1,
|
|
317
|
-
calls: outgoingCalls
|
|
410
|
+
calls: outgoingCalls,
|
|
411
|
+
params, return_type: returnType, class_context: classContext
|
|
318
412
|
});
|
|
319
413
|
}
|
|
320
414
|
} catch (e) {
|
|
@@ -324,6 +418,104 @@ export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
|
324
418
|
return chunks;
|
|
325
419
|
}
|
|
326
420
|
|
|
421
|
+
// ─── Barrel export resolution ─────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
// Module-level cache: barrelAbsPath → Map<exportedName, sourceRelPath>
|
|
424
|
+
const _barrelCache = new Map();
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Parses a barrel file (index.ts / index.js) and returns a map of
|
|
428
|
+
* exportedName → sourceFilePath (relative to projectRoot).
|
|
429
|
+
* e.g. { useAuthStore: 'src/stores/authStore.ts' }
|
|
430
|
+
*/
|
|
431
|
+
export function resolveBarrelExports(barrelAbsPath, projectRoot) {
|
|
432
|
+
if (_barrelCache.has(barrelAbsPath)) return _barrelCache.get(barrelAbsPath);
|
|
433
|
+
|
|
434
|
+
const result = new Map();
|
|
435
|
+
_barrelCache.set(barrelAbsPath, result);
|
|
436
|
+
|
|
437
|
+
const ext = path.extname(barrelAbsPath);
|
|
438
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) return result;
|
|
439
|
+
|
|
440
|
+
let content;
|
|
441
|
+
try { content = fs.readFileSync(barrelAbsPath, 'utf-8'); } catch { return result; }
|
|
442
|
+
|
|
443
|
+
const parser = getParserForFile(ext);
|
|
444
|
+
if (!parser) return result;
|
|
445
|
+
|
|
446
|
+
let tree;
|
|
447
|
+
try { tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null); } catch { return result; }
|
|
448
|
+
|
|
449
|
+
const barrelDir = path.dirname(barrelAbsPath);
|
|
450
|
+
|
|
451
|
+
function walk(node) {
|
|
452
|
+
// export { X, Y as Z } from './source'
|
|
453
|
+
if (node.type === 'export_statement') {
|
|
454
|
+
const fromNode = node.children.find(c => c.type === 'string');
|
|
455
|
+
if (!fromNode) { node.children.forEach(walk); return; }
|
|
456
|
+
|
|
457
|
+
const rawSource = fromNode.text.replace(/['"]/g, '');
|
|
458
|
+
if (!rawSource.startsWith('.')) { node.children.forEach(walk); return; }
|
|
459
|
+
|
|
460
|
+
// Resolve the source file
|
|
461
|
+
const absSource = path.resolve(barrelDir, rawSource);
|
|
462
|
+
let finalAbs = null;
|
|
463
|
+
if (EXTENSIONS.has(path.extname(absSource)) && fs.existsSync(absSource)) {
|
|
464
|
+
finalAbs = absSource;
|
|
465
|
+
} else {
|
|
466
|
+
for (const e of EXTENSIONS) {
|
|
467
|
+
if (fs.existsSync(absSource + e)) { finalAbs = absSource + e; break; }
|
|
468
|
+
const idx = path.join(absSource, 'index' + e);
|
|
469
|
+
if (fs.existsSync(idx)) { finalAbs = idx; break; }
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!finalAbs) { node.children.forEach(walk); return; }
|
|
473
|
+
|
|
474
|
+
const relSource = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
|
|
475
|
+
|
|
476
|
+
// Walk named exports
|
|
477
|
+
const namedExports = node.children.find(c => c.type === 'named_imports' || c.type === 'export_clause');
|
|
478
|
+
if (namedExports) {
|
|
479
|
+
for (const child of namedExports.children) {
|
|
480
|
+
if (child.type === 'import_specifier' || child.type === 'export_specifier') {
|
|
481
|
+
// `alias as exported` or just `name`
|
|
482
|
+
const names = child.children.filter(c => c.type === 'identifier');
|
|
483
|
+
if (names.length > 0) {
|
|
484
|
+
// The exported name is the last identifier (the alias if present)
|
|
485
|
+
result.set(names[names.length - 1].text, relSource);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// export * from './source' → map the source file itself
|
|
492
|
+
const starNode = node.children.find(c => c.text === '*');
|
|
493
|
+
if (starNode) {
|
|
494
|
+
result.set('*', relSource);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
node.children.forEach(walk);
|
|
498
|
+
}
|
|
499
|
+
walk(tree.rootNode);
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ─── Go module-name cache (reads go.mod once per project root) ──────────────
|
|
504
|
+
const _goModCache = new Map();
|
|
505
|
+
function _readGoModuleName(projectRoot) {
|
|
506
|
+
if (_goModCache.has(projectRoot)) return _goModCache.get(projectRoot);
|
|
507
|
+
const modFile = path.join(projectRoot, 'go.mod');
|
|
508
|
+
let name = null;
|
|
509
|
+
if (fs.existsSync(modFile)) {
|
|
510
|
+
try {
|
|
511
|
+
const first = fs.readFileSync(modFile, 'utf-8').split('\n').find(l => l.trimStart().startsWith('module '));
|
|
512
|
+
if (first) name = first.trim().replace(/^module\s+/, '').split(/\s/)[0];
|
|
513
|
+
} catch { /* ignore */ }
|
|
514
|
+
}
|
|
515
|
+
_goModCache.set(projectRoot, name);
|
|
516
|
+
return name;
|
|
517
|
+
}
|
|
518
|
+
|
|
327
519
|
export function resolveLocalImports(rawImports, fromFileRelPath, projectRoot) {
|
|
328
520
|
const fileDir = path.dirname(path.join(projectRoot, fromFileRelPath));
|
|
329
521
|
const ext = path.extname(fromFileRelPath);
|
|
@@ -348,7 +540,46 @@ export function resolveLocalImports(rawImports, fromFileRelPath, projectRoot) {
|
|
|
348
540
|
}
|
|
349
541
|
}
|
|
350
542
|
}
|
|
351
|
-
if (finalAbs)
|
|
543
|
+
if (finalAbs) {
|
|
544
|
+
const relPath = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
|
|
545
|
+
const baseName = path.basename(finalAbs, path.extname(finalAbs));
|
|
546
|
+
// 🥇 BARREL RESOLUTION: If the resolved file is an index file, expand barrel exports
|
|
547
|
+
if (baseName === 'index' && ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(path.extname(finalAbs))) {
|
|
548
|
+
const barrelMap = resolveBarrelExports(finalAbs, projectRoot);
|
|
549
|
+
if (barrelMap.size > 0) {
|
|
550
|
+
// Add all unique source files referenced by this barrel
|
|
551
|
+
const sources = new Set(barrelMap.values());
|
|
552
|
+
for (const src of sources) {
|
|
553
|
+
if (!resolved.includes(src)) resolved.push(src);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// Barrel has no re-exports — keep the barrel file itself
|
|
557
|
+
if (!resolved.includes(relPath)) resolved.push(relPath);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
if (!resolved.includes(relPath)) resolved.push(relPath);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// ── Go intra-module: github.com/owner/repo/sub → sub/*.go ──────────
|
|
565
|
+
// Go import paths use the module name as prefix; map them to local dirs.
|
|
566
|
+
else if (ext === '.go') {
|
|
567
|
+
const modName = _readGoModuleName(projectRoot);
|
|
568
|
+
if (modName && raw.startsWith(modName + '/')) {
|
|
569
|
+
const subPkg = raw.slice(modName.length + 1); // e.g. 'render'
|
|
570
|
+
const absDir = path.join(projectRoot, subPkg);
|
|
571
|
+
if (fs.existsSync(absDir)) {
|
|
572
|
+
try {
|
|
573
|
+
const goFiles = fs.readdirSync(absDir)
|
|
574
|
+
.filter(f => f.endsWith('.go') && !f.includes('_test'))
|
|
575
|
+
.slice(0, 5); // cap: take a representative sample
|
|
576
|
+
for (const gof of goFiles) {
|
|
577
|
+
const rel = path.relative(projectRoot, path.join(absDir, gof)).replace(/\\/g, '/');
|
|
578
|
+
if (!resolved.includes(rel)) resolved.push(rel);
|
|
579
|
+
}
|
|
580
|
+
} catch { /* directory unreadable — skip */ }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
352
583
|
}
|
|
353
584
|
// ── Rust crate-local: crate::module::item → src/module.rs ──────────
|
|
354
585
|
else if (ext === '.rs' && raw.startsWith('crate::')) {
|
|
@@ -419,6 +650,65 @@ export async function getLocalEmbeddingsBatch(texts, graceful = true) {
|
|
|
419
650
|
return null;
|
|
420
651
|
}
|
|
421
652
|
|
|
653
|
+
// ─── Enrichment helpers (param names, return type, class context) ─────────────
|
|
654
|
+
|
|
655
|
+
export function extractParams(chunkNode, ext) {
|
|
656
|
+
const params = [];
|
|
657
|
+
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
658
|
+
const paramTypes = JS_LIKE.includes(ext)
|
|
659
|
+
? ['required_parameter', 'optional_parameter', 'formal_parameters', 'identifier']
|
|
660
|
+
: ['parameter', 'formal_parameter', 'identifier'];
|
|
661
|
+
|
|
662
|
+
function walkParams(node) {
|
|
663
|
+
if (node.type === 'formal_parameters' || node.type === 'parameters' || node.type === 'parameter_list') {
|
|
664
|
+
for (const child of node.children) {
|
|
665
|
+
// TS: required_parameter / optional_parameter have an identifier child
|
|
666
|
+
if (child.type === 'required_parameter' || child.type === 'optional_parameter' || child.type === 'formal_parameter') {
|
|
667
|
+
const id = child.childForFieldName?.('pattern') || child.childForFieldName?.('name') ||
|
|
668
|
+
child.children.find(c => c.type === 'identifier');
|
|
669
|
+
if (id) params.push(id.text);
|
|
670
|
+
// also grab type annotation text
|
|
671
|
+
const typeAnnotation = child.childForFieldName?.('type');
|
|
672
|
+
if (typeAnnotation) {
|
|
673
|
+
const typeText = typeAnnotation.text.replace(/^:\s*/, '').trim();
|
|
674
|
+
if (typeText) params.push(typeText);
|
|
675
|
+
}
|
|
676
|
+
} else if (child.type === 'identifier') {
|
|
677
|
+
params.push(child.text);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
for (const child of node.children) walkParams(child);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Only walk the direct params node to avoid deep recursion into body
|
|
685
|
+
const paramsNode = chunkNode.childForFieldName?.('parameters') || chunkNode.childForFieldName?.('formal_parameters');
|
|
686
|
+
if (paramsNode) walkParams(paramsNode);
|
|
687
|
+
return [...new Set(params)].filter(p => p && p.length > 1).slice(0, 15);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function extractReturnType(chunkNode, ext) {
|
|
691
|
+
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
692
|
+
if (JS_LIKE.includes(ext)) {
|
|
693
|
+
const retTypeNode = chunkNode.childForFieldName?.('return_type');
|
|
694
|
+
if (retTypeNode) return retTypeNode.text.replace(/^:\s*/, '').trim().slice(0, 80);
|
|
695
|
+
}
|
|
696
|
+
return '';
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function extractClassContext(chunkNode) {
|
|
700
|
+
let parent = chunkNode.parent;
|
|
701
|
+
while (parent) {
|
|
702
|
+
if (parent.type === 'class_declaration' || parent.type === 'class_definition' ||
|
|
703
|
+
parent.type === 'class_body' || parent.type === 'impl_item') {
|
|
704
|
+
const nameNode = parent.childForFieldName?.('name');
|
|
705
|
+
if (nameNode) return nameNode.text;
|
|
706
|
+
}
|
|
707
|
+
parent = parent.parent;
|
|
708
|
+
}
|
|
709
|
+
return '';
|
|
710
|
+
}
|
|
711
|
+
|
|
422
712
|
export function extractCalls(rootNode) {
|
|
423
713
|
const calls = new Set();
|
|
424
714
|
function walk(node) {
|
|
@@ -450,6 +740,16 @@ export function extractCalls(rootNode) {
|
|
|
450
740
|
const macroNode = node.childForFieldName?.('macro') || node.children[0];
|
|
451
741
|
if (macroNode && macroNode.type === 'identifier') calls.add(macroNode.text + '!');
|
|
452
742
|
}
|
|
743
|
+
// Java / C#: method_invocation
|
|
744
|
+
else if (node.type === 'method_invocation') {
|
|
745
|
+
const nameNode = node.childForFieldName?.('name') || node.children.find(c => c.type === 'identifier');
|
|
746
|
+
if (nameNode) calls.add(nameNode.text);
|
|
747
|
+
}
|
|
748
|
+
// Ruby: method_call / call
|
|
749
|
+
else if (node.type === 'method_call') {
|
|
750
|
+
const method = node.childForFieldName?.('method') || node.children.find(c => c.type === 'identifier');
|
|
751
|
+
if (method && method.type === 'identifier') calls.add(method.text);
|
|
752
|
+
}
|
|
453
753
|
node.children.forEach(walk);
|
|
454
754
|
}
|
|
455
755
|
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,9 +114,18 @@ 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
|
|