@kurtel/cli 0.1.13 → 0.1.14
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/dist/commands/onboard.js +1 -1
- package/dist/memory/ast.js +365 -0
- package/dist/memory/indexer.js +32 -18
- package/package.json +1 -1
package/dist/commands/onboard.js
CHANGED
|
@@ -9,7 +9,7 @@ export async function onboardCommand(opts = {}) {
|
|
|
9
9
|
const root = repoRoot();
|
|
10
10
|
// 1. Index structurel — local, déterministe, 0 token.
|
|
11
11
|
const spin = opts.json ? null : new Spinner("Indexing codebase (local, deterministic)…").start();
|
|
12
|
-
const index = buildIndex(root, (n) => {
|
|
12
|
+
const index = await buildIndex(root, (n) => {
|
|
13
13
|
spin?.update(`Indexing codebase… ${n} files`);
|
|
14
14
|
});
|
|
15
15
|
saveIndex(root, index);
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
let initFailed = false;
|
|
6
|
+
let parser = null;
|
|
7
|
+
const languages = new Map(); // grammarName → Language
|
|
8
|
+
const missing = new Set();
|
|
9
|
+
function grammarFor(ext) {
|
|
10
|
+
switch (ext) {
|
|
11
|
+
case ".ts":
|
|
12
|
+
case ".mts":
|
|
13
|
+
case ".cts": return "typescript";
|
|
14
|
+
case ".tsx": return "tsx";
|
|
15
|
+
case ".js":
|
|
16
|
+
case ".jsx":
|
|
17
|
+
case ".mjs":
|
|
18
|
+
case ".cjs": return "javascript"; // jsx couvert par la grammaire JS
|
|
19
|
+
case ".py": return "python";
|
|
20
|
+
default: return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function getParser(ext) {
|
|
24
|
+
if (initFailed)
|
|
25
|
+
return null;
|
|
26
|
+
const grammar = grammarFor(ext);
|
|
27
|
+
if (!grammar || missing.has(grammar))
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
if (!parser) {
|
|
31
|
+
const Parser = require("web-tree-sitter");
|
|
32
|
+
await Parser.init();
|
|
33
|
+
parser = new Parser();
|
|
34
|
+
getParser.ParserClass = Parser;
|
|
35
|
+
}
|
|
36
|
+
let lang = languages.get(grammar);
|
|
37
|
+
if (!lang) {
|
|
38
|
+
const wasmDir = join(dirname(require.resolve("tree-sitter-wasms/package.json")), "out");
|
|
39
|
+
const wasmPath = join(wasmDir, `tree-sitter-${grammar}.wasm`);
|
|
40
|
+
if (!existsSync(wasmPath)) {
|
|
41
|
+
missing.add(grammar);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const Parser = getParser.ParserClass;
|
|
45
|
+
lang = await Parser.Language.load(wasmPath);
|
|
46
|
+
languages.set(grammar, lang);
|
|
47
|
+
}
|
|
48
|
+
return { parser, lang };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
initFailed = true; // environnement sans WASM: fallback regex global, une seule tentative
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ── Helpers communs ──────────────────────────────────────────────────────────
|
|
56
|
+
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options", "all"]);
|
|
57
|
+
const ROUTE_OBJECTS = new Set(["app", "router", "server", "api", "fastify"]);
|
|
58
|
+
const NEST_DECORATORS = new Set(["Get", "Post", "Put", "Patch", "Delete", "Head", "Options"]);
|
|
59
|
+
function stripQuotes(s) {
|
|
60
|
+
return s.replace(/^["'`]|["'`]$/g, "");
|
|
61
|
+
}
|
|
62
|
+
function line(n) { return n.startPosition.row + 1; }
|
|
63
|
+
/** Ligne de la définition nommée englobante (0 = top-level / handler anonyme → "(module)").
|
|
64
|
+
* Renvoie la ligne du NŒUD PORTEUR DU NOM (def enregistrée dans facts.defs), pour
|
|
65
|
+
* que l'attribution par `owner` matche `byLine` côté buildSymbols. */
|
|
66
|
+
function enclosingDefLine(node, defKinds, nameOf) {
|
|
67
|
+
let cur = node.parent;
|
|
68
|
+
while (cur) {
|
|
69
|
+
if (defKinds.has(cur.type)) {
|
|
70
|
+
const name = nameOf(cur);
|
|
71
|
+
if (name) {
|
|
72
|
+
// La def est enregistrée à la ligne du déclarateur/méthode, pas de l'arrow.
|
|
73
|
+
if (cur.type === "arrow_function" || cur.type === "function" || cur.type === "function_expression") {
|
|
74
|
+
const carrier = cur.parent; // variable_declarator | public_field_definition
|
|
75
|
+
return line(carrier ?? cur);
|
|
76
|
+
}
|
|
77
|
+
return line(cur);
|
|
78
|
+
}
|
|
79
|
+
// fonction anonyme (callback inline): on continue de remonter
|
|
80
|
+
}
|
|
81
|
+
cur = cur.parent;
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
function emptyFacts(rel, src) {
|
|
86
|
+
return {
|
|
87
|
+
rel, loc: src.split("\n").length, exports: [], importSpecs: [], routes: [],
|
|
88
|
+
defs: [], namedImports: {}, rawCalls: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ── TypeScript / JavaScript ──────────────────────────────────────────────────
|
|
92
|
+
const JS_DEF_KINDS = new Set(["function_declaration", "generator_function_declaration", "method_definition", "arrow_function", "function", "function_expression"]);
|
|
93
|
+
function jsDefName(n) {
|
|
94
|
+
if (n.type === "function_declaration" || n.type === "generator_function_declaration" || n.type === "method_definition") {
|
|
95
|
+
return n.childForFieldName("name");
|
|
96
|
+
}
|
|
97
|
+
// arrow/function expression: nommée seulement si assignée directement à un déclarateur
|
|
98
|
+
const p = n.parent;
|
|
99
|
+
if (p?.type === "variable_declarator")
|
|
100
|
+
return p.childForFieldName("name");
|
|
101
|
+
if (p?.type === "public_field_definition")
|
|
102
|
+
return p.childForFieldName("name");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function extractJs(rel, src, root) {
|
|
106
|
+
const facts = emptyFacts(rel, src);
|
|
107
|
+
// Imports (statiques + re-exports + require)
|
|
108
|
+
for (const imp of root.descendantsOfType("import_statement")) {
|
|
109
|
+
const source = imp.childForFieldName("source");
|
|
110
|
+
if (!source)
|
|
111
|
+
continue;
|
|
112
|
+
const spec = stripQuotes(source.text);
|
|
113
|
+
facts.importSpecs.push(spec);
|
|
114
|
+
for (const clause of imp.descendantsOfType("import_clause")) {
|
|
115
|
+
for (const child of clause.namedChildren) {
|
|
116
|
+
if (child.type === "identifier") {
|
|
117
|
+
facts.namedImports[child.text] = spec; // default import
|
|
118
|
+
}
|
|
119
|
+
else if (child.type === "namespace_import") {
|
|
120
|
+
const id = child.descendantsOfType("identifier")[0]; // import * as utils
|
|
121
|
+
if (id)
|
|
122
|
+
facts.namedImports[id.text] = spec;
|
|
123
|
+
}
|
|
124
|
+
else if (child.type === "named_imports") {
|
|
125
|
+
for (const isp of child.descendantsOfType("import_specifier")) {
|
|
126
|
+
// local = alias si présent, sinon name. On ignore les imports de type.
|
|
127
|
+
if (isp.children.some((c) => c.type === "type"))
|
|
128
|
+
continue;
|
|
129
|
+
const nameNode = isp.childForFieldName("name");
|
|
130
|
+
const aliasNode = isp.childForFieldName("alias");
|
|
131
|
+
const local = (aliasNode ?? nameNode)?.text;
|
|
132
|
+
const orig = nameNode?.text;
|
|
133
|
+
if (local && orig)
|
|
134
|
+
facts.namedImports[local] = local === orig ? spec : { spec, orig };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const exp of root.descendantsOfType("export_statement")) {
|
|
141
|
+
const source = exp.childForFieldName("source");
|
|
142
|
+
if (source)
|
|
143
|
+
facts.importSpecs.push(stripQuotes(source.text)); // re-export = arête
|
|
144
|
+
const decl = exp.childForFieldName("declaration");
|
|
145
|
+
const name = decl?.childForFieldName("name");
|
|
146
|
+
if (name)
|
|
147
|
+
facts.exports.push(name.text);
|
|
148
|
+
for (const spec of exp.descendantsOfType("export_specifier")) {
|
|
149
|
+
const n = (spec.childForFieldName("alias") ?? spec.childForFieldName("name"))?.text;
|
|
150
|
+
if (n)
|
|
151
|
+
facts.exports.push(n);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Définitions
|
|
155
|
+
for (const fn of root.descendantsOfType(["function_declaration", "generator_function_declaration"])) {
|
|
156
|
+
const name = fn.childForFieldName("name");
|
|
157
|
+
if (name)
|
|
158
|
+
facts.defs.push({ name: name.text, line: line(fn) });
|
|
159
|
+
}
|
|
160
|
+
for (const decl of root.descendantsOfType("variable_declarator")) {
|
|
161
|
+
const value = decl.childForFieldName("value");
|
|
162
|
+
const name = decl.childForFieldName("name");
|
|
163
|
+
if (name?.type === "identifier" && value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression")) {
|
|
164
|
+
facts.defs.push({ name: name.text, line: line(decl) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const m of root.descendantsOfType("method_definition")) {
|
|
168
|
+
const name = m.childForFieldName("name");
|
|
169
|
+
if (name && name.text !== "constructor")
|
|
170
|
+
facts.defs.push({ name: name.text, line: line(m) });
|
|
171
|
+
}
|
|
172
|
+
for (const f of root.descendantsOfType("public_field_definition")) {
|
|
173
|
+
const value = f.childForFieldName("value");
|
|
174
|
+
const name = f.childForFieldName("name");
|
|
175
|
+
if (name && value?.type === "arrow_function")
|
|
176
|
+
facts.defs.push({ name: name.text, line: line(f) });
|
|
177
|
+
}
|
|
178
|
+
for (const c of root.descendantsOfType("class_declaration")) {
|
|
179
|
+
const name = c.childForFieldName("name");
|
|
180
|
+
if (name)
|
|
181
|
+
facts.exports.push(name.text);
|
|
182
|
+
}
|
|
183
|
+
// Appels + routes
|
|
184
|
+
for (const call of root.descendantsOfType("call_expression")) {
|
|
185
|
+
const fn = call.childForFieldName("function");
|
|
186
|
+
if (!fn)
|
|
187
|
+
continue;
|
|
188
|
+
if (fn.type === "identifier") {
|
|
189
|
+
if (fn.text === "require") {
|
|
190
|
+
const arg = call.childForFieldName("arguments")?.namedChildren[0];
|
|
191
|
+
if (arg && (arg.type === "string" || arg.type === "template_string"))
|
|
192
|
+
facts.importSpecs.push(stripQuotes(arg.text));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
facts.rawCalls.push({ name: fn.text, line: line(call), owner: enclosingDefLine(call, JS_DEF_KINDS, jsDefName) });
|
|
196
|
+
}
|
|
197
|
+
else if (fn.type === "member_expression") {
|
|
198
|
+
const obj = fn.childForFieldName("object");
|
|
199
|
+
const prop = fn.childForFieldName("property");
|
|
200
|
+
if (!obj || !prop)
|
|
201
|
+
continue;
|
|
202
|
+
// Route express-like: app.get("/x", ...)
|
|
203
|
+
if (obj.type === "identifier" && ROUTE_OBJECTS.has(obj.text) && HTTP_METHODS.has(prop.text)) {
|
|
204
|
+
const arg = call.childForFieldName("arguments")?.namedChildren[0];
|
|
205
|
+
if (arg && (arg.type === "string" || arg.type === "template_string")) {
|
|
206
|
+
facts.routes.push({ method: prop.text.toUpperCase(), path: stripQuotes(arg.text), file: rel, line: line(call), framework: "express-like" });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Appel qualifié résoluble: ImportéOuClasse.method(...)
|
|
210
|
+
if (obj.type === "identifier") {
|
|
211
|
+
facts.rawCalls.push({ name: obj.text + ".", line: line(call), owner: enclosingDefLine(call, JS_DEF_KINDS, jsDefName) });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Décorateurs Nest: @Get("/x")
|
|
216
|
+
for (const dec of root.descendantsOfType("decorator")) {
|
|
217
|
+
const call = dec.namedChildren[0];
|
|
218
|
+
if (call?.type !== "call_expression")
|
|
219
|
+
continue;
|
|
220
|
+
const fn = call.childForFieldName("function");
|
|
221
|
+
if (fn?.type === "identifier" && NEST_DECORATORS.has(fn.text)) {
|
|
222
|
+
const arg = call.childForFieldName("arguments")?.namedChildren[0];
|
|
223
|
+
facts.routes.push({
|
|
224
|
+
method: fn.text.toUpperCase(),
|
|
225
|
+
path: arg && (arg.type === "string") ? stripQuotes(arg.text) : "",
|
|
226
|
+
file: rel, line: line(dec), framework: "nest",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return facts;
|
|
231
|
+
}
|
|
232
|
+
// ── Python ───────────────────────────────────────────────────────────────────
|
|
233
|
+
const PY_DEF_KINDS = new Set(["function_definition"]);
|
|
234
|
+
const pyDefName = (n) => n.childForFieldName("name");
|
|
235
|
+
/** "from .sub import x" → spec JS-style "./sub" résoluble par le résolveur existant. */
|
|
236
|
+
function pyRelativeSpec(dots, mod) {
|
|
237
|
+
const up = dots <= 1 ? "./" : "../".repeat(dots - 1);
|
|
238
|
+
return up + mod.replace(/\./g, "/");
|
|
239
|
+
}
|
|
240
|
+
function extractPy(rel, src, root) {
|
|
241
|
+
const facts = emptyFacts(rel, src);
|
|
242
|
+
for (const imp of root.descendantsOfType("import_from_statement")) {
|
|
243
|
+
// forme: from <relative_import|dotted_name> import a, b as c | from . import service
|
|
244
|
+
const modNode = imp.childForFieldName("module_name");
|
|
245
|
+
let spec = "";
|
|
246
|
+
let relative = false;
|
|
247
|
+
if (modNode) {
|
|
248
|
+
if (modNode.type === "relative_import") {
|
|
249
|
+
relative = true;
|
|
250
|
+
const dots = (modNode.text.match(/^\.+/) ?? ["."])[0].length;
|
|
251
|
+
const mod = modNode.text.slice(dots);
|
|
252
|
+
spec = pyRelativeSpec(dots, mod);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
spec = modNode.text;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const names = [];
|
|
259
|
+
for (const child of imp.namedChildren) {
|
|
260
|
+
if (child === modNode)
|
|
261
|
+
continue;
|
|
262
|
+
if (child.type === "dotted_name")
|
|
263
|
+
names.push(child.text);
|
|
264
|
+
if (child.type === "aliased_import") {
|
|
265
|
+
const alias = child.childForFieldName("alias");
|
|
266
|
+
if (alias)
|
|
267
|
+
names.push(alias.text);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (relative && !spec.replace(/^\.\/|\.\.\//g, "")) {
|
|
271
|
+
// "from . import service" → chaque nom est un module frère
|
|
272
|
+
for (const n of names) {
|
|
273
|
+
const s = pyRelativeSpec(1, n);
|
|
274
|
+
facts.importSpecs.push(s);
|
|
275
|
+
facts.namedImports[n] = s;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (spec) {
|
|
279
|
+
facts.importSpecs.push(spec);
|
|
280
|
+
for (const n of names)
|
|
281
|
+
facts.namedImports[n] = spec;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const imp of root.descendantsOfType("import_statement")) {
|
|
285
|
+
for (const d of imp.descendantsOfType("dotted_name"))
|
|
286
|
+
facts.importSpecs.push(d.text);
|
|
287
|
+
}
|
|
288
|
+
for (const fn of root.descendantsOfType("function_definition")) {
|
|
289
|
+
const name = fn.childForFieldName("name");
|
|
290
|
+
if (name) {
|
|
291
|
+
facts.defs.push({ name: name.text, line: line(fn) });
|
|
292
|
+
facts.exports.push(name.text);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const cls of root.descendantsOfType("class_definition")) {
|
|
296
|
+
const name = cls.childForFieldName("name");
|
|
297
|
+
if (name)
|
|
298
|
+
facts.exports.push(name.text);
|
|
299
|
+
}
|
|
300
|
+
for (const call of root.descendantsOfType("call")) {
|
|
301
|
+
const fn = call.childForFieldName("function");
|
|
302
|
+
if (!fn)
|
|
303
|
+
continue;
|
|
304
|
+
if (fn.type === "identifier") {
|
|
305
|
+
facts.rawCalls.push({ name: fn.text, line: line(call), owner: enclosingDefLine(call, PY_DEF_KINDS, pyDefName) });
|
|
306
|
+
}
|
|
307
|
+
else if (fn.type === "attribute") {
|
|
308
|
+
const obj = fn.childForFieldName("object");
|
|
309
|
+
const attr = fn.childForFieldName("attribute");
|
|
310
|
+
if (obj?.type === "identifier" && attr) {
|
|
311
|
+
// Route FastAPI/Flask: app.get("/x") en position de décorateur
|
|
312
|
+
if (call.parent?.type === "decorator" && (HTTP_METHODS.has(attr.text) || attr.text === "route")) {
|
|
313
|
+
const arg = call.childForFieldName("arguments")?.namedChildren[0];
|
|
314
|
+
if (arg?.type === "string") {
|
|
315
|
+
facts.routes.push({
|
|
316
|
+
method: attr.text === "route" ? "*" : attr.text.toUpperCase(),
|
|
317
|
+
path: stripQuotes(arg.text), file: rel, line: line(call), framework: "python",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
facts.rawCalls.push({ name: obj.text + ".", line: line(call), owner: enclosingDefLine(call, PY_DEF_KINDS, pyDefName) });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return facts;
|
|
326
|
+
}
|
|
327
|
+
// ── Point d'entrée ───────────────────────────────────────────────────────────
|
|
328
|
+
const MAX_DEFS = 40;
|
|
329
|
+
const MAX_CALLS = 1000;
|
|
330
|
+
/** Extraction AST. Retourne null si tree-sitter indisponible pour ce fichier → fallback regex. */
|
|
331
|
+
export async function extractFileAst(rel, src, ext) {
|
|
332
|
+
const ctx = await getParser(ext);
|
|
333
|
+
if (!ctx)
|
|
334
|
+
return null;
|
|
335
|
+
let tree = null;
|
|
336
|
+
try {
|
|
337
|
+
ctx.parser.setLanguage(ctx.lang);
|
|
338
|
+
tree = ctx.parser.parse(src);
|
|
339
|
+
const facts = ext === ".py" ? extractPy(rel, src, tree.rootNode) : extractJs(rel, src, tree.rootNode);
|
|
340
|
+
// Bornes + dédupe defs (première occurrence par nom), tri par ligne — comme la voie regex.
|
|
341
|
+
const seen = new Set();
|
|
342
|
+
facts.defs = facts.defs
|
|
343
|
+
.sort((a, b) => a.line - b.line)
|
|
344
|
+
.filter((d) => (seen.has(d.name) ? false : (seen.add(d.name), true)))
|
|
345
|
+
.slice(0, MAX_DEFS);
|
|
346
|
+
facts.rawCalls = facts.rawCalls.slice(0, MAX_CALLS).sort((a, b) => a.line - b.line);
|
|
347
|
+
facts.routes = facts.routes.slice(0, 200);
|
|
348
|
+
facts.exports = [...new Set(facts.exports)];
|
|
349
|
+
return facts;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return null; // fichier illisible par l'AST → regex
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
tree?.delete(); // mémoire WASM: indispensable sur des milliers de fichiers
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Routes par convention de fichier Next.js — indépendant du parseur, partagé. */
|
|
359
|
+
export function nextRouteFor(rel) {
|
|
360
|
+
const nm = rel.replace(/\\/g, "/").match(/(^|\/)(pages|app)\/(.+?)\/?(route|page|index)?\.(ts|tsx|js|jsx)$/);
|
|
361
|
+
if (!nm)
|
|
362
|
+
return null;
|
|
363
|
+
const urlPath = "/" + nm[3].replace(/\[(\w+)\]/g, ":$1").replace(/\/index$/, "");
|
|
364
|
+
return { method: "*", path: urlPath, file: rel, line: 1, framework: "next" };
|
|
365
|
+
}
|
package/dist/memory/indexer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join, relative, extname, dirname } from "node:path";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { repoFullName } from "./store.js";
|
|
5
|
+
import { extractFileAst, nextRouteFor } from "./ast.js";
|
|
5
6
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
7
|
// Indexeur structurel — 100% local, 0 token, déterministe (même input → même
|
|
7
8
|
// output). Heuristiques regex pragmatiques pour TS/JS/Python ; le point
|
|
@@ -40,7 +41,6 @@ const PY_ROUTE_PATTERNS = [
|
|
|
40
41
|
path: (m) => m[2],
|
|
41
42
|
},
|
|
42
43
|
];
|
|
43
|
-
const NEXT_ROUTE_FILE = /(^|\/)(pages|app)\/(.+?)\/?(route|page|index)?\.(ts|tsx|js|jsx)$/;
|
|
44
44
|
const JS_KEYWORDS = new Set([
|
|
45
45
|
"if", "for", "while", "switch", "catch", "return", "function", "new", "typeof",
|
|
46
46
|
"await", "async", "import", "export", "require", "console", "constructor",
|
|
@@ -154,12 +154,6 @@ function extractFile(root, rel, src) {
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
// Routes par convention de fichier (Next.js app/pages router).
|
|
158
|
-
const nm = rel.replace(/\\/g, "/").match(NEXT_ROUTE_FILE);
|
|
159
|
-
if (nm) {
|
|
160
|
-
const urlPath = "/" + nm[3].replace(/\[(\w+)\]/g, ":$1").replace(/\/index$/, "");
|
|
161
|
-
facts.routes.push({ method: "*", path: urlPath, file: rel, line: 1, framework: "next" });
|
|
162
|
-
}
|
|
163
157
|
return facts;
|
|
164
158
|
}
|
|
165
159
|
// ── Résolution d'imports internes (TS/JS relatifs + Python par module path) ──
|
|
@@ -221,22 +215,30 @@ function buildSymbols(all, resolve) {
|
|
|
221
215
|
const name = qualified ? raw.slice(0, -1) : raw;
|
|
222
216
|
if (!qualified && locals.has(name))
|
|
223
217
|
return `${rel}::${name}`;
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
218
|
+
const entry = f.namedImports[name];
|
|
219
|
+
if (entry) {
|
|
220
|
+
const spec = typeof entry === "string" ? entry : entry.spec;
|
|
221
|
+
const orig = typeof entry === "string" ? name : entry.orig; // alias → nom exporté côté cible
|
|
226
222
|
const file = resolve(rel, spec);
|
|
227
|
-
if (file && file !== rel && (defNames.get(file)?.has(
|
|
228
|
-
return `${file}::${
|
|
223
|
+
if (file && file !== rel && (defNames.get(file)?.has(orig) || exportNames.get(file)?.has(orig))) {
|
|
224
|
+
return `${file}::${orig}`;
|
|
229
225
|
}
|
|
230
226
|
}
|
|
231
227
|
return null;
|
|
232
228
|
};
|
|
229
|
+
const byLine = new Map(symbols.map((s) => [s.line, s]));
|
|
233
230
|
for (const call of f.rawCalls) {
|
|
234
231
|
let caller = topLevel;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
232
|
+
if (call.owner !== undefined) {
|
|
233
|
+
caller = call.owner === 0 ? topLevel : (byLine.get(call.owner) ?? topLevel);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
for (const s of symbols) {
|
|
237
|
+
if (s.line <= call.line)
|
|
238
|
+
caller = s;
|
|
239
|
+
else
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
240
242
|
}
|
|
241
243
|
if (caller.name === call.name || caller.calls.length >= MAX_CALLS_PER_SYMBOL)
|
|
242
244
|
continue;
|
|
@@ -281,13 +283,24 @@ function* walk(dir, root) {
|
|
|
281
283
|
}
|
|
282
284
|
}
|
|
283
285
|
// ── Construction de l'index ─────────────────────────────────────────────────
|
|
284
|
-
export function buildIndex(root, onProgress) {
|
|
286
|
+
export async function buildIndex(root, onProgress) {
|
|
285
287
|
const files = new Map();
|
|
286
288
|
let n = 0;
|
|
289
|
+
let regexFallbacks = 0;
|
|
287
290
|
for (const rel of walk(root, root)) {
|
|
288
291
|
try {
|
|
289
292
|
const src = readFileSync(join(root, rel), "utf8");
|
|
290
|
-
|
|
293
|
+
const ast = await extractFileAst(rel, src, extname(rel));
|
|
294
|
+
if (ast) {
|
|
295
|
+
files.set(rel, ast);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
files.set(rel, extractFile(root, rel, src));
|
|
299
|
+
regexFallbacks++;
|
|
300
|
+
}
|
|
301
|
+
const next = nextRouteFor(rel);
|
|
302
|
+
if (next)
|
|
303
|
+
files.get(rel).routes.push(next);
|
|
291
304
|
if (onProgress && ++n % 50 === 0)
|
|
292
305
|
onProgress(n);
|
|
293
306
|
}
|
|
@@ -338,6 +351,7 @@ export function buildIndex(root, onProgress) {
|
|
|
338
351
|
catch { /* pas un repo git */ }
|
|
339
352
|
return {
|
|
340
353
|
version: 1,
|
|
354
|
+
parser: { engine: regexFallbacks === files.size ? "regex" : "tree-sitter", regex_fallbacks: regexFallbacks },
|
|
341
355
|
repo: repoFullName(root),
|
|
342
356
|
commit,
|
|
343
357
|
generated_at: new Date().toISOString(),
|