graph-indexer 1.2.0 → 2.0.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.
- package/CHANGELOG.md +412 -0
- package/README.md +216 -596
- package/SECURITY.md +68 -0
- package/TRADEMARK.md +31 -0
- package/cli-ui.mjs +78 -0
- package/config.mjs +179 -28
- package/daemon-ctl.mjs +175 -0
- package/daemon-lock.mjs +69 -0
- package/embedders/python/mlx_embed_server.py +61 -0
- package/embedders/python/requirements-mlx.txt +5 -0
- package/embedders/setup-mlx.mjs +110 -0
- package/embeddings.mjs +489 -0
- package/engine/binary.mjs +399 -0
- package/{core-engine.mjs → engine/memory.mjs} +107 -440
- package/{sqlite-store.mjs → engine/sqlite.mjs} +137 -42
- package/enrichment.mjs +111 -32
- package/git-signals.mjs +248 -0
- package/indexer.mjs +151 -42
- package/init.mjs +1603 -171
- package/layout.mjs +123 -0
- package/mcp/format.mjs +61 -0
- package/mcp/tools.mjs +1157 -0
- package/mcp/topology.mjs +551 -0
- package/mcp-server.mjs +55 -24
- package/package.json +44 -13
- package/parse/extractor.mjs +617 -0
- package/parse/imports.mjs +365 -0
- package/parse/languages.mjs +89 -0
- package/parse/metadata.mjs +655 -0
- package/parse/routes.mjs +327 -0
- package/prompts/CORE.md +86 -0
- package/prompts/DOMAIN_TEMPLATE.md +46 -0
- package/prompts/INTEGRATION.md +123 -0
- package/prompts/frameworks/ANDROID.md +17 -0
- package/prompts/frameworks/ASPNET_CORE.md +19 -0
- package/prompts/frameworks/FASTAPI_DJANGO.md +19 -0
- package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
- package/prompts/frameworks/RAILS.md +20 -0
- package/prompts/frameworks/REACT.md +20 -0
- package/prompts/frameworks/SPRING_BOOT.md +19 -0
- package/prompts/languages/BASH.md +22 -0
- package/prompts/languages/C.md +21 -0
- package/prompts/languages/CSHARP.md +24 -0
- package/prompts/languages/CSS_SCSS.md +22 -0
- package/prompts/languages/GO.md +23 -0
- package/prompts/languages/JAVA.md +23 -0
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
- package/prompts/languages/KOTLIN.md +24 -0
- package/prompts/languages/PHP.md +23 -0
- package/prompts/languages/PYTHON.md +23 -0
- package/prompts/languages/RUBY.md +23 -0
- package/prompts/languages/RUST.md +21 -0
- package/prompts/languages/SWIFT.md +21 -0
- package/search-core.mjs +254 -43
- package/storage.mjs +28 -3
- package/watch-daemon.mjs +69 -23
- package/PROMPT.md +0 -182
- package/mcp-tools.mjs +0 -545
- package/parser-utils.mjs +0 -1170
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file parse/extractor.mjs
|
|
3
|
+
* @description File-level AST extraction: ignore filter, chunk hashing,
|
|
4
|
+
* file skeleton, import extraction, and semantic chunking.
|
|
5
|
+
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
|
+
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import ignore from 'ignore';
|
|
13
|
+
import Parser from 'tree-sitter';
|
|
14
|
+
const { Query } = Parser;
|
|
15
|
+
|
|
16
|
+
import { getParserForFile, LANGUAGE_MAP, EXTENSIONS } from './languages.mjs';
|
|
17
|
+
import {
|
|
18
|
+
extractParams, extractReturnType, extractClassContext,
|
|
19
|
+
extractDecorators, extractHeritage, extractTypeAnnotations, extractCallSites,
|
|
20
|
+
} from './metadata.mjs';
|
|
21
|
+
|
|
22
|
+
export const MAX_FILE_SIZE_BYTES = 500000;
|
|
23
|
+
|
|
24
|
+
const _hasSCSS = Boolean(LANGUAGE_MAP['.scss']) && LANGUAGE_MAP['.scss'] !== LANGUAGE_MAP['.css'];
|
|
25
|
+
|
|
26
|
+
const LANGUAGE_QUERIES = {
|
|
27
|
+
ts: `
|
|
28
|
+
(class_declaration) @chunk
|
|
29
|
+
(function_declaration) @chunk
|
|
30
|
+
(method_definition) @chunk
|
|
31
|
+
(lexical_declaration) @chunk
|
|
32
|
+
(expression_statement) @chunk
|
|
33
|
+
(export_statement) @chunk
|
|
34
|
+
`,
|
|
35
|
+
css: `(rule_set) @chunk`,
|
|
36
|
+
scss: `
|
|
37
|
+
(rule_set) @chunk
|
|
38
|
+
(mixin_statement) @chunk
|
|
39
|
+
(function_statement) @chunk
|
|
40
|
+
`,
|
|
41
|
+
py: `
|
|
42
|
+
(function_definition) @chunk
|
|
43
|
+
(class_definition) @chunk
|
|
44
|
+
(expression_statement) @chunk
|
|
45
|
+
`,
|
|
46
|
+
rs: `
|
|
47
|
+
(function_item) @chunk
|
|
48
|
+
(struct_item) @chunk
|
|
49
|
+
(enum_item) @chunk
|
|
50
|
+
(trait_item) @chunk
|
|
51
|
+
(impl_item) @chunk
|
|
52
|
+
`,
|
|
53
|
+
go: `
|
|
54
|
+
(function_declaration) @chunk
|
|
55
|
+
(method_declaration) @chunk
|
|
56
|
+
(type_declaration) @chunk
|
|
57
|
+
`,
|
|
58
|
+
php: `
|
|
59
|
+
(function_definition) @chunk
|
|
60
|
+
(method_declaration) @chunk
|
|
61
|
+
(class_declaration) @chunk
|
|
62
|
+
(expression_statement) @chunk
|
|
63
|
+
`,
|
|
64
|
+
java: `
|
|
65
|
+
(method_declaration) @chunk
|
|
66
|
+
(class_declaration) @chunk
|
|
67
|
+
(interface_declaration) @chunk
|
|
68
|
+
(constructor_declaration) @chunk
|
|
69
|
+
(enum_declaration) @chunk
|
|
70
|
+
`,
|
|
71
|
+
kotlin: `
|
|
72
|
+
(function_declaration) @chunk
|
|
73
|
+
(class_declaration) @chunk
|
|
74
|
+
(object_declaration) @chunk
|
|
75
|
+
(companion_object) @chunk
|
|
76
|
+
(secondary_constructor) @chunk
|
|
77
|
+
`,
|
|
78
|
+
cs: `
|
|
79
|
+
(method_declaration) @chunk
|
|
80
|
+
(class_declaration) @chunk
|
|
81
|
+
(interface_declaration) @chunk
|
|
82
|
+
(constructor_declaration) @chunk
|
|
83
|
+
(enum_declaration) @chunk
|
|
84
|
+
(property_declaration) @chunk
|
|
85
|
+
`,
|
|
86
|
+
rb: `
|
|
87
|
+
(method) @chunk
|
|
88
|
+
(singleton_method) @chunk
|
|
89
|
+
(class) @chunk
|
|
90
|
+
(module) @chunk
|
|
91
|
+
`,
|
|
92
|
+
// C: functions + record/typedef definitions. Bare prototypes (declaration) and
|
|
93
|
+
// object macros (preproc_def) are intentionally excluded — too noisy / sub-chunk.
|
|
94
|
+
c: `
|
|
95
|
+
(function_definition) @chunk
|
|
96
|
+
(struct_specifier) @chunk
|
|
97
|
+
(union_specifier) @chunk
|
|
98
|
+
(enum_specifier) @chunk
|
|
99
|
+
(type_definition) @chunk
|
|
100
|
+
(preproc_function_def) @chunk
|
|
101
|
+
`,
|
|
102
|
+
// Bash: the function is the only meaningful indexable unit. Top-level command
|
|
103
|
+
// sequences are script glue, not symbols, so they are not chunked.
|
|
104
|
+
bash: `
|
|
105
|
+
(function_definition) @chunk
|
|
106
|
+
`,
|
|
107
|
+
// Swift: struct/class/enum/extension all parse as class_declaration (the grammar
|
|
108
|
+
// distinguishes them by an inner keyword); protocols and free funcs are separate.
|
|
109
|
+
swift: `
|
|
110
|
+
(class_declaration) @chunk
|
|
111
|
+
(protocol_declaration) @chunk
|
|
112
|
+
(function_declaration) @chunk
|
|
113
|
+
`
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const CONTAINERS = new Set([
|
|
117
|
+
'class_declaration', 'function_declaration', 'method_definition',
|
|
118
|
+
'lexical_declaration', 'expression_statement', 'export_statement',
|
|
119
|
+
'function_definition', 'class_definition', 'rule_set',
|
|
120
|
+
// SCSS
|
|
121
|
+
'mixin_statement', 'function_statement',
|
|
122
|
+
'function_item', 'struct_item', 'enum_item', 'trait_item', 'impl_item',
|
|
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',
|
|
132
|
+
// C (records + typedef so nested/anonymous specifiers dedupe to one chunk)
|
|
133
|
+
'struct_specifier', 'union_specifier', 'enum_specifier', 'type_definition',
|
|
134
|
+
'preproc_function_def',
|
|
135
|
+
// Swift (class_declaration + function_declaration already listed above)
|
|
136
|
+
'protocol_declaration'
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
export function buildIgnoreFilter(rootPath) {
|
|
140
|
+
const ig = ignore();
|
|
141
|
+
ig.add(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '*.tmp', 'vendor', '.venv']);
|
|
142
|
+
const gitignorePath = path.join(rootPath, '.gitignore');
|
|
143
|
+
if (fs.existsSync(gitignorePath)) ig.add(fs.readFileSync(gitignorePath, 'utf-8'));
|
|
144
|
+
return ig;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function generateChunkHash(text) {
|
|
148
|
+
return createHash('sha256').update(text).digest('hex');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function extractFileSkeleton(rootNode, content) {
|
|
152
|
+
const signatures = [];
|
|
153
|
+
function walk(node) {
|
|
154
|
+
if (node.type.includes('declaration') || node.type.includes('definition')) {
|
|
155
|
+
let name = node.childForFieldName?.("name")?.text || "anonymous";
|
|
156
|
+
signatures.push(`- [${node.type}] ${name} (lines ${node.startPosition.row + 1}-${node.endPosition.row + 1})`);
|
|
157
|
+
}
|
|
158
|
+
node.children.forEach(walk);
|
|
159
|
+
}
|
|
160
|
+
walk(rootNode);
|
|
161
|
+
return signatures.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function extractImportsFromAST(rootNode, ext) {
|
|
165
|
+
const imports = new Set();
|
|
166
|
+
function walk(node) {
|
|
167
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
168
|
+
if (node.type === 'import_statement') {
|
|
169
|
+
const source = node.children.find(c => c.type === 'string');
|
|
170
|
+
if (source) imports.add(source.text.replace(/['"]/g, ''));
|
|
171
|
+
} else if (node.type === 'call_expression' && node.children[0]?.text === 'require') {
|
|
172
|
+
const arg = node.children[1]?.children?.find(c => c.type === 'string');
|
|
173
|
+
if (arg) imports.add(arg.text.replace(/['"]/g, ''));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (ext === '.py') {
|
|
177
|
+
if (node.type === 'import_statement') {
|
|
178
|
+
// import foo.bar.baz → store as foo/bar/baz
|
|
179
|
+
for (const child of node.children) {
|
|
180
|
+
if (child.type === 'dotted_name') imports.add(child.text.replace(/\./g, '/'));
|
|
181
|
+
}
|
|
182
|
+
} else if (node.type === 'import_from_statement') {
|
|
183
|
+
// from .sibling import X → relative: emit as ./sibling
|
|
184
|
+
// from ..pkg import Y → relative: emit as ../pkg
|
|
185
|
+
// from foo.bar import Z → absolute: store foo/bar
|
|
186
|
+
const relNode = node.children.find(c => c.type === 'relative_import');
|
|
187
|
+
if (relNode) {
|
|
188
|
+
// dots count = depth; remainder is the module path after dots
|
|
189
|
+
const raw = relNode.text; // e.g. '.' or '..utils'
|
|
190
|
+
const dots = raw.match(/^\.+/)?.[0] ?? '.';
|
|
191
|
+
const mod = raw.slice(dots.length);
|
|
192
|
+
const prefix = dots.length === 1 ? './' : '../'.repeat(dots.length - 1);
|
|
193
|
+
imports.add(mod ? prefix + mod.replace(/\./g, '/') : prefix.slice(0, -1) || '.');
|
|
194
|
+
} else {
|
|
195
|
+
const mod = node.children.find(c => c.type === 'dotted_name');
|
|
196
|
+
if (mod) imports.add(mod.text.replace(/\./g, '/'));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (ext === '.rs') {
|
|
201
|
+
if (node.type === 'use_declaration') {
|
|
202
|
+
// Collect the first path text from the argument subtree
|
|
203
|
+
const arg = node.childForFieldName?.('argument') ||
|
|
204
|
+
node.children.find(c => !['use', ';', 'pub'].includes(c.type));
|
|
205
|
+
if (arg) imports.add(arg.text.split('::').slice(0, 3).join('::'));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (ext === '.go') {
|
|
209
|
+
if (node.type === 'import_spec') {
|
|
210
|
+
const pathNode = node.children.find(c =>
|
|
211
|
+
c.type === 'interpreted_string_literal' || c.type === 'raw_string_literal');
|
|
212
|
+
if (pathNode) imports.add(pathNode.text.replace(/['"`]/g, ''));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (ext === '.php') {
|
|
216
|
+
if (node.type === 'include_expression' || node.type === 'require_expression' ||
|
|
217
|
+
node.type === 'include_once_expression' || node.type === 'require_once_expression') {
|
|
218
|
+
const strNode = node.children.find(c =>
|
|
219
|
+
c.type === 'string' || c.type === 'encapsed_string');
|
|
220
|
+
if (strNode) imports.add(strNode.text.replace(/['"]/g, ''));
|
|
221
|
+
}
|
|
222
|
+
} else if (ext === '.java') {
|
|
223
|
+
if (node.type === 'import_declaration') {
|
|
224
|
+
const scopedId = node.children.find(c => c.type === 'scoped_identifier' || c.type === 'identifier');
|
|
225
|
+
if (scopedId) imports.add(scopedId.text.replace(/\./g, '/'));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (ext === '.kt' || ext === '.kts') {
|
|
229
|
+
if (node.type === 'import_header') {
|
|
230
|
+
const path = node.children.find(c => c.type === 'identifier' || c.type === 'user_type' || c.isNamed);
|
|
231
|
+
const raw = node.text.replace(/^import\s+/, '').replace(/\s*\.\*\s*$/, '').trim();
|
|
232
|
+
if (raw) imports.add(raw.replace(/\./g, '/'));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (ext === '.cs') {
|
|
236
|
+
if (node.type === 'using_directive') {
|
|
237
|
+
const ns = node.children.find(c => c.type === 'qualified_name' || c.type === 'identifier' || c.type === 'name_equals');
|
|
238
|
+
if (ns) {
|
|
239
|
+
const raw = ns.text.replace(/\s*=\s*.*$/, '').trim();
|
|
240
|
+
if (raw) imports.add(raw.replace(/\./g, '/'));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (ext === '.rb') {
|
|
245
|
+
if (node.type === 'call' || node.type === 'method_call') {
|
|
246
|
+
const method = node.childForFieldName?.('method') || node.children[0];
|
|
247
|
+
if (method && (method.text === 'require' || method.text === 'require_relative')) {
|
|
248
|
+
const args = node.childForFieldName?.('arguments') || node.children.find(c => c.type === 'argument_list');
|
|
249
|
+
const strArg = args?.children?.find(c => c.type === 'string' || c.type === 'simple_string');
|
|
250
|
+
if (strArg) imports.add(strArg.text.replace(/['"]/g, ''));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (ext === '.c' || ext === '.h') {
|
|
255
|
+
// Only quoted local includes (`#include "foo.h"`) carry intra-project
|
|
256
|
+
// edges; angle-bracket system includes (<stdio.h>) are stdlib noise.
|
|
257
|
+
if (node.type === 'preproc_include') {
|
|
258
|
+
const str = node.children.find(c => c.type === 'string_literal');
|
|
259
|
+
if (str) imports.add(str.text.replace(/^[<"]|[>"]$/g, ''));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else if (ext === '.sh' || ext === '.bash') {
|
|
263
|
+
// `source path` / `. path` pull another script into scope.
|
|
264
|
+
if (node.type === 'command') {
|
|
265
|
+
const cmd = node.childForFieldName?.('name')?.text;
|
|
266
|
+
if (cmd === 'source' || cmd === '.') {
|
|
267
|
+
const arg = node.namedChildren.find(c => c.type !== 'command_name');
|
|
268
|
+
if (arg) imports.add(arg.text.replace(/['"]/g, ''));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (ext === '.swift') {
|
|
273
|
+
// `import Foundation` / `import MyModule.Submodule` — module-level, rarely
|
|
274
|
+
// file-resolvable, but tracked so the dependency list is non-empty.
|
|
275
|
+
if (node.type === 'import_declaration') {
|
|
276
|
+
const raw = node.text.replace(/^@?\w*\s*import\s+/, '').trim();
|
|
277
|
+
if (raw) imports.add(raw.replace(/\./g, '/'));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
node.children.forEach(walk);
|
|
281
|
+
}
|
|
282
|
+
walk(rootNode);
|
|
283
|
+
return Array.from(imports);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── God-class defence ────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Token-safe skeleton for a class that exceeds GOD_CLASS_LINES.
|
|
290
|
+
*
|
|
291
|
+
* The agent prompt (prompts/CORE.md) promises get_chunk() costs ~300 tokens. A 2 000-line
|
|
292
|
+
* "god class" stored as one chunk violates that contract. This skeleton keeps
|
|
293
|
+
* only the first HEADER_LINES of the class (signature + opening brace) and
|
|
294
|
+
* appends a one-line summary — enough for name resolution and embeddings, while
|
|
295
|
+
* the real bodies are reachable via the individual method chunks that the
|
|
296
|
+
* god-class split produces.
|
|
297
|
+
*/
|
|
298
|
+
function buildGodClassSkeleton(classNode) {
|
|
299
|
+
const allLines = classNode.text.split('\n');
|
|
300
|
+
const HEADER_LINES = 15;
|
|
301
|
+
const header = allLines.slice(0, HEADER_LINES).join('\n');
|
|
302
|
+
return (
|
|
303
|
+
`${header}\n` +
|
|
304
|
+
` // ⚠ [Large class: ${allLines.length} lines — ` +
|
|
305
|
+
`methods are indexed as individual searchable chunks. ` +
|
|
306
|
+
`Use search_code() to find specific methods.]\n}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Resolve the declared name out of a C declarator chain.
|
|
312
|
+
* C nests the identifier under layers of pointer_declarator / array_declarator /
|
|
313
|
+
* function_declarator (`int *make_node(void)` → pointer_declarator → function_declarator
|
|
314
|
+
* → identifier), so we descend the `declarator` field until we hit the identifier.
|
|
315
|
+
*/
|
|
316
|
+
function _cDeclaratorName(node) {
|
|
317
|
+
let d = node, guard = 0;
|
|
318
|
+
while (d && guard++ < 16) {
|
|
319
|
+
if (d.type === 'identifier' || d.type === 'type_identifier' || d.type === 'field_identifier') return d.text;
|
|
320
|
+
// Descend the declarator field, or — for wrappers without that field, e.g. a
|
|
321
|
+
// function-pointer typedef's parenthesized_declarator — the inner *_declarator.
|
|
322
|
+
const next = d.childForFieldName?.('declarator')
|
|
323
|
+
|| d.namedChildren?.find(c => /_declarator$/.test(c.type));
|
|
324
|
+
if (next && next !== d) { d = next; continue; }
|
|
325
|
+
const id = d.namedChildren?.find(c =>
|
|
326
|
+
c.type === 'identifier' || c.type === 'type_identifier' || c.type === 'field_identifier');
|
|
327
|
+
if (id) return id.text;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function extractSemanticChunks(rootNode, relPath, sourceCode, ext) {
|
|
334
|
+
const chunks = [];
|
|
335
|
+
const parser = getParserForFile(ext);
|
|
336
|
+
if (!parser) return chunks;
|
|
337
|
+
|
|
338
|
+
const JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
339
|
+
const EXT_TO_LANG = {
|
|
340
|
+
'.java': 'java', '.kt': 'kotlin', '.kts': 'kotlin', '.cs': 'cs',
|
|
341
|
+
'.rb': 'rb',
|
|
342
|
+
// .scss uses the SCSS grammar+query when installed; otherwise it parses
|
|
343
|
+
// with the CSS grammar, so it MUST use the css query (the scss query
|
|
344
|
+
// references node types the CSS grammar doesn't have).
|
|
345
|
+
'.scss': _hasSCSS ? 'scss' : 'css',
|
|
346
|
+
// C headers share the C grammar/query; .sh and .bash share the bash query.
|
|
347
|
+
// (.c and .swift resolve via the ext.slice(1) fallback below.)
|
|
348
|
+
'.h': 'c', '.sh': 'bash', '.bash': 'bash',
|
|
349
|
+
};
|
|
350
|
+
const langKey = JS_LIKE.includes(ext) ? 'ts'
|
|
351
|
+
: (EXT_TO_LANG[ext] || (LANGUAGE_QUERIES[ext.slice(1)] ? ext.slice(1) : null));
|
|
352
|
+
if (!langKey || !LANGUAGE_QUERIES[langKey]) return chunks;
|
|
353
|
+
|
|
354
|
+
// Extract top-of-file comments for module context (header inheritance).
|
|
355
|
+
let fileDocstring = "";
|
|
356
|
+
let cursorNode = rootNode.children[0];
|
|
357
|
+
while (cursorNode) {
|
|
358
|
+
if (cursorNode.type === 'comment') fileDocstring += cursorNode.text + "\n";
|
|
359
|
+
else if (cursorNode.type !== 'import_statement' && cursorNode.type !== 'expression_statement') break;
|
|
360
|
+
cursorNode = cursorNode.nextSibling;
|
|
361
|
+
}
|
|
362
|
+
fileDocstring = fileDocstring.trim();
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const query = new Query(parser.getLanguage(), LANGUAGE_QUERIES[langKey]);
|
|
366
|
+
const matches = query.matches(rootNode);
|
|
367
|
+
const processedNodes = new Set();
|
|
368
|
+
|
|
369
|
+
// ── God-class pre-pass ─────────────────────────────────────────────────
|
|
370
|
+
// TS exported classes live inside export_statement — mark the inner
|
|
371
|
+
// class_declaration so the method-isNested check resolves the correct ancestor.
|
|
372
|
+
const GOD_CLASS_LINES = 200;
|
|
373
|
+
const GOD_CLASS_NODE_TYPES = new Set([
|
|
374
|
+
'class_declaration', 'class_definition',
|
|
375
|
+
'impl_item', // Rust impl blocks
|
|
376
|
+
'class', // Ruby
|
|
377
|
+
'object_declaration', // Kotlin
|
|
378
|
+
]);
|
|
379
|
+
const oversizedClassIds = new Set(); // node IDs whose direct methods are un-nested
|
|
380
|
+
|
|
381
|
+
for (const match of matches) {
|
|
382
|
+
for (const capture of match.captures) {
|
|
383
|
+
if (capture.name !== 'chunk') continue;
|
|
384
|
+
const n = capture.node;
|
|
385
|
+
const nLines = n.endPosition.row - n.startPosition.row + 1;
|
|
386
|
+
if (nLines <= GOD_CLASS_LINES) continue;
|
|
387
|
+
if (GOD_CLASS_NODE_TYPES.has(n.type)) {
|
|
388
|
+
oversizedClassIds.add(n.id);
|
|
389
|
+
}
|
|
390
|
+
// TS: large export_statement wrapping a class — mark the inner class_declaration
|
|
391
|
+
// so method_definition.parent chain finds the oversized class node, not the
|
|
392
|
+
// export_statement (which isn't what the isNested walk stops at).
|
|
393
|
+
if (n.type === 'export_statement') {
|
|
394
|
+
for (let ci = 0; ci < n.namedChildCount; ci++) {
|
|
395
|
+
const c = n.namedChild(ci);
|
|
396
|
+
if (GOD_CLASS_NODE_TYPES.has(c.type)) {
|
|
397
|
+
oversizedClassIds.add(c.id);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const match of matches) {
|
|
406
|
+
let chunkNode = null;
|
|
407
|
+
for (const capture of match.captures) {
|
|
408
|
+
if (capture.name === 'chunk') chunkNode = capture.node;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!chunkNode || processedNodes.has(chunkNode.id)) continue;
|
|
412
|
+
processedNodes.add(chunkNode.id);
|
|
413
|
+
|
|
414
|
+
// Skip trivial definitions spanning fewer than 3 lines (one-line getters,
|
|
415
|
+
// re-export forwarders, stubs): they add symbol noise and rarely carry
|
|
416
|
+
// behaviour worth its own chunk. A file of ONLY one-liners thus yields no
|
|
417
|
+
// chunks — the indexer surfaces a warning when the whole repo comes back empty.
|
|
418
|
+
if (chunkNode.endPosition.row - chunkNode.startPosition.row < 2) continue;
|
|
419
|
+
|
|
420
|
+
// Walk to parent === null (tree root) rather than stopping at a named root type:
|
|
421
|
+
// Ruby's `module` keyword is in CONTAINERS but also the name of Python's root node,
|
|
422
|
+
// so stopping at 'program' alone falsely marked top-level Python classes as nested.
|
|
423
|
+
// God-class exception: if the first CONTAINERS ancestor is oversized, the node is a
|
|
424
|
+
// direct method — allow it through as its own chunk.
|
|
425
|
+
let isNested = false;
|
|
426
|
+
let currentParent = chunkNode.parent;
|
|
427
|
+
while (currentParent && currentParent.parent !== null) {
|
|
428
|
+
if (CONTAINERS.has(currentParent.type)) {
|
|
429
|
+
if (!oversizedClassIds.has(currentParent.id)) {
|
|
430
|
+
isNested = true;
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
currentParent = currentParent.parent;
|
|
435
|
+
}
|
|
436
|
+
if (isNested) continue;
|
|
437
|
+
|
|
438
|
+
let nameText = "anonymous";
|
|
439
|
+
const nameNode = chunkNode.childForFieldName?.("name");
|
|
440
|
+
|
|
441
|
+
if (nameNode) {
|
|
442
|
+
nameText = nameNode.text;
|
|
443
|
+
} else if (chunkNode.type === "export_statement") {
|
|
444
|
+
const decl = chunkNode.children.find(c => ['lexical_declaration', 'function_declaration', 'class_declaration'].includes(c.type));
|
|
445
|
+
if (decl) {
|
|
446
|
+
if (decl.type === "lexical_declaration") {
|
|
447
|
+
const varDecl = decl.children.find(c => c.type === "variable_declarator");
|
|
448
|
+
nameText = varDecl?.children.find(c => c.type === "identifier")?.text || "anonymous";
|
|
449
|
+
} else {
|
|
450
|
+
nameText = decl.childForFieldName?.("name")?.text || "anonymous";
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
const defaultChild = chunkNode.children.find(c => c.type === 'identifier' || c.type === 'call_expression');
|
|
454
|
+
if (defaultChild) {
|
|
455
|
+
if (defaultChild.type === 'identifier') {
|
|
456
|
+
nameText = `default_${defaultChild.text}`;
|
|
457
|
+
} else if (defaultChild.type === 'call_expression') {
|
|
458
|
+
const funcName = defaultChild.childForFieldName?.("function")?.text || defaultChild.children[0]?.text;
|
|
459
|
+
const argNode = defaultChild.childForFieldName?.("arguments")?.children?.find(c => c.type === 'identifier');
|
|
460
|
+
nameText = argNode ? `default_${funcName}_${argNode.text}` : `default_${funcName}`;
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
nameText = "default_export";
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
} else if (chunkNode.type === "lexical_declaration") {
|
|
467
|
+
const decl = chunkNode.children.find(c => c.type === "variable_declarator");
|
|
468
|
+
nameText = decl?.children.find(c => c.type === "identifier")?.text || "anonymous";
|
|
469
|
+
} else if (chunkNode.type === "expression_statement") {
|
|
470
|
+
const callExp = chunkNode.children.find(c => c.type === "call_expression");
|
|
471
|
+
const assignExp = chunkNode.children.find(c => c.type === "assignment_expression");
|
|
472
|
+
if (callExp) {
|
|
473
|
+
const funcName = callExp.childForFieldName?.("function")?.text || callExp.children[0]?.text;
|
|
474
|
+
const argsNode = callExp.childForFieldName?.("arguments");
|
|
475
|
+
const stringArg = argsNode?.children?.find(c => c.type === "string" || c.type === "template_string");
|
|
476
|
+
nameText = stringArg ? `${funcName}_${stringArg.text.replace(/['"`]/g, '')}` : (funcName || "anonymous");
|
|
477
|
+
} else if (assignExp) {
|
|
478
|
+
nameText = assignExp.childForFieldName?.("left")?.text || "anonymous";
|
|
479
|
+
}
|
|
480
|
+
} else if (chunkNode.type === "type_declaration") {
|
|
481
|
+
// Go: `type X struct {…}` / `type X interface {…}` / `type X = Y`.
|
|
482
|
+
// The identifier lives on the nested type_spec / type_alias node, not on
|
|
483
|
+
// the type_declaration itself, so childForFieldName("name") above is null.
|
|
484
|
+
// Without this branch every Go struct/interface collapses to the useless
|
|
485
|
+
// synthetic name `<file>_type_declaration`, making core types (e.g. Gin's
|
|
486
|
+
// RouterGroup, Engine, Context) unsearchable by name and invisible to the
|
|
487
|
+
// 2.0× name-boost in searchHybrid.
|
|
488
|
+
const spec = chunkNode.namedChildren?.find(c => c.type === "type_spec" || c.type === "type_alias");
|
|
489
|
+
nameText = spec?.childForFieldName?.("name")?.text
|
|
490
|
+
|| spec?.children?.find(c => c.type === "type_identifier")?.text
|
|
491
|
+
|| "anonymous";
|
|
492
|
+
} else if (chunkNode.type === "function_definition" || chunkNode.type === "type_definition"
|
|
493
|
+
|| chunkNode.type === "preproc_function_def") {
|
|
494
|
+
// C: the name is not a direct `name` field. For a function it is buried in
|
|
495
|
+
// declarator → … → identifier (e.g. `int *make()` nests through a
|
|
496
|
+
// pointer_declarator); for a typedef the `declarator` field is the new type
|
|
497
|
+
// name; a function-like macro carries an `identifier` child. Without this the
|
|
498
|
+
// chunk collapses to the useless synthetic `<file>_function_definition`, making
|
|
499
|
+
// every C function unsearchable by name and invisible to the 2.0× name boost.
|
|
500
|
+
nameText = _cDeclaratorName(chunkNode.childForFieldName?.("declarator"))
|
|
501
|
+
|| chunkNode.children.find(c => c.type === "identifier")?.text
|
|
502
|
+
|| "anonymous";
|
|
503
|
+
} else {
|
|
504
|
+
// Generic fallback: search direct children for an identifier-like node.
|
|
505
|
+
// Includes type_identifier (Go/TS), constant (Ruby class/module names) and
|
|
506
|
+
// field_identifier so nested-name grammars don't fall through to anonymous.
|
|
507
|
+
const idNode = chunkNode.children.find(c =>
|
|
508
|
+
c.type === "identifier" || c.type === "name" || c.type === "property_identifier"
|
|
509
|
+
|| c.type === "type_identifier" || c.type === "constant" || c.type === "field_identifier");
|
|
510
|
+
nameText = idNode?.text || "anonymous";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (nameText === "anonymous" || nameText === "default_export") {
|
|
514
|
+
nameText = `${path.basename(relPath, path.extname(relPath))}_${chunkNode.type}`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let docstring = "";
|
|
518
|
+
let prev = chunkNode.previousSibling;
|
|
519
|
+
while (prev && (prev.type === 'comment' || !prev.isNamed)) {
|
|
520
|
+
if (prev.type === 'comment') docstring = prev.text + "\n" + docstring;
|
|
521
|
+
prev = prev.previousSibling;
|
|
522
|
+
}
|
|
523
|
+
docstring = docstring.trim();
|
|
524
|
+
|
|
525
|
+
if (!docstring && fileDocstring) {
|
|
526
|
+
docstring = `[File Context]: ${fileDocstring}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// For oversized class containers — or TS export_statements wrapping one —
|
|
530
|
+
// store a compact skeleton so get_chunk() returns a bounded token count.
|
|
531
|
+
// All metadata fields (calls, params, type_refs, decorators, extends) are
|
|
532
|
+
// still extracted from the FULL AST node, so ranking and topology are intact.
|
|
533
|
+
const isOversizedClass = oversizedClassIds.has(chunkNode.id);
|
|
534
|
+
const wrapsOversizedClass = !isOversizedClass && chunkNode.type === 'export_statement' && (() => {
|
|
535
|
+
for (let ci = 0; ci < chunkNode.namedChildCount; ci++) {
|
|
536
|
+
if (oversizedClassIds.has(chunkNode.namedChild(ci).id)) return true;
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
})();
|
|
540
|
+
const snippet = (isOversizedClass || wrapsOversizedClass)
|
|
541
|
+
? buildGodClassSkeleton(isOversizedClass ? chunkNode : (() => {
|
|
542
|
+
for (let ci = 0; ci < chunkNode.namedChildCount; ci++) {
|
|
543
|
+
const c = chunkNode.namedChild(ci);
|
|
544
|
+
if (oversizedClassIds.has(c.id)) return c;
|
|
545
|
+
}
|
|
546
|
+
return chunkNode;
|
|
547
|
+
})())
|
|
548
|
+
: chunkNode.text.slice(0, 3000);
|
|
549
|
+
// Hash the FULL node text (not the 3000-char snippet) so the content key
|
|
550
|
+
// reflects a definition's whole body. This keeps the dense channel's window
|
|
551
|
+
// vectors (which embed the full body — see buildEmbeddingPayload/embeddingWindows)
|
|
552
|
+
// correctly invalidated when a tail-only edit changes code past the snippet
|
|
553
|
+
// cap. For chunks whose body fits the cap, node text == snippet, so the hash
|
|
554
|
+
// is unchanged (no needless re-embed); BM25 and code_snippet are untouched.
|
|
555
|
+
const hash = generateChunkHash(docstring + chunkNode.text);
|
|
556
|
+
const callSites = extractCallSites(chunkNode).slice(0, 256);
|
|
557
|
+
const outgoingCalls = Array.from(new Set(callSites.map(s => s.name)));
|
|
558
|
+
|
|
559
|
+
const params = extractParams(chunkNode, ext);
|
|
560
|
+
const returnType = extractReturnType(chunkNode, ext);
|
|
561
|
+
const classContext = extractClassContext(chunkNode);
|
|
562
|
+
const typeRefs = extractTypeAnnotations(chunkNode, ext);
|
|
563
|
+
const decorators = extractDecorators(chunkNode);
|
|
564
|
+
const heritage = extractHeritage(chunkNode, ext);
|
|
565
|
+
|
|
566
|
+
const id = createHash('sha256')
|
|
567
|
+
.update(`${relPath}::${chunkNode.startPosition.row}::${chunkNode.startPosition.column}`)
|
|
568
|
+
.digest('hex').slice(0, 24);
|
|
569
|
+
|
|
570
|
+
chunks.push({
|
|
571
|
+
id, file_path: relPath, node_type: chunkNode.type, name: nameText,
|
|
572
|
+
docstring: docstring, code_snippet: snippet, content_hash: hash,
|
|
573
|
+
start_line: chunkNode.startPosition.row + 1, end_line: chunkNode.endPosition.row + 1,
|
|
574
|
+
calls: outgoingCalls, call_sites: callSites,
|
|
575
|
+
params, return_type: returnType, class_context: classContext,
|
|
576
|
+
type_refs: typeRefs, decorators, extends: heritage,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
} catch (e) {
|
|
580
|
+
process.stderr.write(`\n[parse/extractor] 💥 Query Error in ${relPath}: ${e.message}\n`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ── Python public re-exports ────────────────────────────────────────────────
|
|
584
|
+
// PEP 484 convention: `from starlette.background import BackgroundTasks as
|
|
585
|
+
// BackgroundTasks` re-exports a symbol as public API. Files like
|
|
586
|
+
// fastapi/background.py consist ONLY of such lines and previously produced
|
|
587
|
+
// zero chunks — resolve_symbol('BackgroundTasks') found nothing and agents
|
|
588
|
+
// hit a dead end. Each explicit re-export becomes a small chunk that names
|
|
589
|
+
// the symbol and points at its source module. (JS/TS barrels already chunk
|
|
590
|
+
// via the export_statement capture.)
|
|
591
|
+
if (ext === '.py') {
|
|
592
|
+
for (const node of rootNode.children) {
|
|
593
|
+
if (node.type !== 'import_from_statement') continue;
|
|
594
|
+
const moduleName = node.childForFieldName?.('module_name')?.text || '';
|
|
595
|
+
for (const child of node.children) {
|
|
596
|
+
if (child.type !== 'aliased_import') continue;
|
|
597
|
+
const orig = child.childForFieldName?.('name')?.text || '';
|
|
598
|
+
const alias = child.childForFieldName?.('alias')?.text || '';
|
|
599
|
+
if (!alias || orig.split('.').pop() !== alias) continue; // only `import X as X`
|
|
600
|
+
const snippet = node.text.slice(0, 300);
|
|
601
|
+
const docstring = `Public re-export of ${alias} from ${moduleName}.`;
|
|
602
|
+
const id = createHash('sha256')
|
|
603
|
+
.update(`${relPath}::${node.startPosition.row}::${child.startPosition.column}`)
|
|
604
|
+
.digest('hex').slice(0, 24);
|
|
605
|
+
chunks.push({
|
|
606
|
+
id, file_path: relPath, node_type: 're_export', name: alias,
|
|
607
|
+
docstring, code_snippet: snippet,
|
|
608
|
+
content_hash: generateChunkHash(docstring + snippet),
|
|
609
|
+
start_line: node.startPosition.row + 1, end_line: node.endPosition.row + 1,
|
|
610
|
+
calls: [], call_sites: [], params: [], return_type: '', class_context: '',
|
|
611
|
+
type_refs: [], decorators: [], extends: [],
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return chunks;
|
|
617
|
+
}
|