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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "1.0.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 tests/eval-harness.mjs --index ./code-index.json",
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": "GPL-3.0-only",
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 GPL-3.0-only
7
- * * This program is free software: you can redistribute it and/or modify
8
- * it under the terms of the GNU General Public License as published by
9
- * the Free Software Foundation, either version 3 of the License, or
10
- * (at your option) any later version.
11
- *
12
- * This program is distributed in the hope that it will be useful,
13
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- * GNU General Public License for more details.
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; // Importación robusta nativa
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 langKey = JS_LIKE.includes(ext) ? 'ts' : (LANGUAGE_QUERIES[ext.slice(1)] ? ext.slice(1) : null);
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.type !== 'program') {
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) resolved.push(path.relative(projectRoot, finalAbs).replace(/\\/g, '/'));
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 GPL-3.0-only
8
- * This program is free software: you can redistribute it and/or modify
9
- * it under the terms of the GNU General Public License as published by
10
- * the Free Software Foundation, either version 3 of the License, or
11
- * (at your option) any later version.
12
- *
13
- * This program is distributed with the hope that it will be beneficial,
14
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
- * GNU General Public License for more details.
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 = `${chunk.name} ${chunk.docstring || ''} ${cleanDeps.join(' ')} ${(chunk.calls || []).join(' ')} ${chunk.code_snippet}`;
118
-
119
- db._indexLexical(chunk.id, enrichedContext);
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 en ${filename}: ${err.message}\n`);
135
+ process.stderr.write(`[daemon] ❌ Error in ${filename}: ${err.message}\n`);
127
136
  }
128
137
  }
129
138
 
130
- process.stderr.write(`🚀 Chokidar Watcher Daemon iniciado en: ${PROJECT_ROOT}\n`);
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 }