brito-cli 0.0.0-stage → 0.3.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.
@@ -0,0 +1,526 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export const EXTRA_EXT = {
4
+ ".java": "java", ".kt": "kotlin", ".go": "go", ".rb": "ruby", ".php": "php", ".cs": "c_sharp", ".rs": "rust",
5
+ };
6
+ export const isExtraLang = (l) => l in HOOKS;
7
+ const f = (n, name) => n.childForFieldName(name);
8
+ const nameOf = (n) => f(n, "name")?.text || null;
9
+ const lastSeg = (s) => s.split(/[.\\/:]+/).filter(Boolean).pop() ?? s;
10
+ const SELF_TYPES = new Set(["this", "this_expression", "self"]);
11
+ const IDENT_TYPES = new Set(["identifier", "simple_identifier", "constant", "type_identifier", "name", "field_identifier", "property_identifier"]);
12
+ /** x.y / x->y / x::y: separa a base do nome do membro, em qualquer gramática. */
13
+ function member(n) {
14
+ switch (n.type) {
15
+ case "field_access": {
16
+ const b = f(n, "object"), m = f(n, "field");
17
+ return b && m ? { base: b, name: m.text } : null;
18
+ }
19
+ case "member_access_expression":
20
+ case "nullsafe_member_access_expression": {
21
+ const b = f(n, "object") ?? f(n, "expression"), m = f(n, "name");
22
+ return b && m ? { base: b, name: m.text } : null;
23
+ }
24
+ case "field_expression": {
25
+ const b = f(n, "value"), m = f(n, "field");
26
+ return b && m ? { base: b, name: m.text } : null;
27
+ }
28
+ case "selector_expression": {
29
+ const b = f(n, "operand"), m = f(n, "field");
30
+ return b && m ? { base: b, name: m.text } : null;
31
+ }
32
+ case "navigation_expression": {
33
+ const b = n.namedChild(0), m = n.namedChildren.filter((c) => c.type === "navigation_suffix").pop()?.namedChild(0);
34
+ return b && m ? { base: b, name: m.text } : null;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ const isSelf = (n, self) => SELF_TYPES.has(n.type) || n.text === "$this" || (!!self && n.type === "identifier" && n.text === self);
40
+ /** O "obj" de uma chamada: this, o nome da variável/tipo, o nome do campo (this.repo.x() → "repo") ou "?". */
41
+ function objOf(o, self) {
42
+ if (!o)
43
+ return "";
44
+ if (isSelf(o, self))
45
+ return "this";
46
+ if (IDENT_TYPES.has(o.type) || o.type === "variable_name")
47
+ return o.text.replace(/^\$/, "");
48
+ if (o.type === "scoped_identifier" || o.type === "qualified_name" || o.type === "scope_resolution")
49
+ return lastSeg(o.text);
50
+ const m = member(o);
51
+ if (m && isSelf(m.base, self))
52
+ return m.name;
53
+ return "?";
54
+ }
55
+ function ofTypes(map) {
56
+ return (n) => {
57
+ const kind = map[n.type];
58
+ const name = kind ? nameOf(n) : null;
59
+ return name ? { name, kind } : null;
60
+ };
61
+ }
62
+ // ───────────── Java ─────────────
63
+ const java = {
64
+ implicitSelf: true,
65
+ type: ofTypes({ class_declaration: "class", interface_declaration: "interface", enum_declaration: "enum", record_declaration: "class", annotation_type_declaration: "interface" }),
66
+ func: (n) => (n.type === "method_declaration" || n.type === "constructor_declaration") && nameOf(n) ? { name: nameOf(n) } : null,
67
+ call: (n, self) => {
68
+ if (n.type !== "method_invocation")
69
+ return null;
70
+ const name = nameOf(n);
71
+ return name ? { name, obj: objOf(f(n, "object"), self) } : null;
72
+ },
73
+ imports: (n, acc) => {
74
+ if (n.type !== "import_declaration")
75
+ return;
76
+ const id = n.namedChildren.find((c) => c.type === "scoped_identifier" || c.type === "identifier");
77
+ if (!id)
78
+ return;
79
+ acc.imports.push(id.text);
80
+ if (!n.children.some((c) => c.type === "asterisk"))
81
+ acc.bindings.push([lastSeg(id.text), id.text, lastSeg(id.text)]);
82
+ },
83
+ };
84
+ // ───────────── Kotlin ─────────────
85
+ const kotlin = {
86
+ implicitSelf: true,
87
+ type: (n) => {
88
+ if (n.type !== "class_declaration" && n.type !== "object_declaration")
89
+ return null;
90
+ const name = n.namedChildren.find((c) => c.type === "type_identifier")?.text;
91
+ if (!name)
92
+ return null;
93
+ const kind = n.children.some((c) => c.type === "interface") ? "interface" : /\benum\b/.test(n.namedChildren.find((c) => c.type === "modifiers")?.text ?? "") ? "enum" : "class";
94
+ return { name, kind };
95
+ },
96
+ func: (n) => {
97
+ const name = n.type === "function_declaration" ? n.namedChildren.find((c) => c.type === "simple_identifier")?.text : null;
98
+ return name ? { name } : null;
99
+ },
100
+ call: (n, self) => {
101
+ if (n.type !== "call_expression")
102
+ return null;
103
+ const callee = n.namedChild(0);
104
+ if (!callee)
105
+ return null;
106
+ if (callee.type === "simple_identifier")
107
+ return { name: callee.text, obj: "" };
108
+ const m = member(callee);
109
+ return m ? { name: m.name, obj: objOf(m.base, self) } : null;
110
+ },
111
+ imports: (n, acc) => {
112
+ if (n.type !== "import_header")
113
+ return;
114
+ const id = n.namedChildren.find((c) => c.type === "identifier");
115
+ if (!id)
116
+ return;
117
+ acc.imports.push(id.text);
118
+ if (!n.text.includes("*")) {
119
+ const alias = n.namedChildren.find((c) => c.type === "import_alias")?.namedChild(0)?.text;
120
+ acc.bindings.push([alias ?? lastSeg(id.text), id.text, lastSeg(id.text)]);
121
+ }
122
+ },
123
+ };
124
+ // ───────────── Go ─────────────
125
+ /** Nome do tipo do receiver: `(s *Service)` → "Service". */
126
+ function goReceiver(n) {
127
+ const p = f(n, "receiver")?.namedChildren.find((c) => c.type === "parameter_declaration");
128
+ if (!p)
129
+ return null;
130
+ let t = f(p, "type");
131
+ while (t && t.type !== "type_identifier")
132
+ t = t.namedChild(0);
133
+ return t ? { type: t.text, self: f(p, "name")?.text ?? null } : null;
134
+ }
135
+ const go = {
136
+ implicitSelf: false,
137
+ type: (n) => {
138
+ if (n.type !== "type_spec" && n.type !== "type_alias")
139
+ return null;
140
+ const name = nameOf(n);
141
+ if (!name)
142
+ return null;
143
+ const t = f(n, "type")?.type;
144
+ return { name, kind: t === "struct_type" ? "struct" : t === "interface_type" ? "interface" : "type" };
145
+ },
146
+ func: (n) => {
147
+ const name = nameOf(n);
148
+ if (!name)
149
+ return null;
150
+ if (n.type === "function_declaration")
151
+ return { name };
152
+ if (n.type !== "method_declaration")
153
+ return null;
154
+ const r = goReceiver(n);
155
+ return { name, cls: r?.type, self: r?.self ?? null };
156
+ },
157
+ call: (n, self) => {
158
+ if (n.type !== "call_expression")
159
+ return null;
160
+ const fn = f(n, "function");
161
+ if (!fn)
162
+ return null;
163
+ if (fn.type === "identifier")
164
+ return { name: fn.text, obj: "" };
165
+ const m = member(fn);
166
+ return m ? { name: m.name, obj: objOf(m.base, self) } : null;
167
+ },
168
+ imports: (n, acc) => {
169
+ if (n.type !== "import_spec")
170
+ return;
171
+ const spec = f(n, "path")?.text.replace(/^["`]|["`]$/g, "");
172
+ if (!spec)
173
+ return;
174
+ acc.imports.push(spec);
175
+ const alias = f(n, "name")?.text;
176
+ if (alias === "_" || alias === ".")
177
+ return;
178
+ const segs = spec.split("/");
179
+ const last = /^v\d+$/.test(segs[segs.length - 1]) && segs.length > 1 ? segs[segs.length - 2] : segs[segs.length - 1];
180
+ acc.bindings.push([alias ?? last.replace(/^go-|\.go$/g, "").replace(/[.-].*$/, ""), spec, "*"]);
181
+ },
182
+ };
183
+ // ───────────── Ruby ─────────────
184
+ const RUBY_SKIP = new Set(["require", "require_relative", "load", "include", "extend", "prepend", "attr_accessor", "attr_reader", "attr_writer", "private", "public", "protected", "module_function", "raise", "puts", "p", "print"]);
185
+ const ruby = {
186
+ implicitSelf: true,
187
+ type: (n) => {
188
+ if (n.type !== "class" && n.type !== "module")
189
+ return null;
190
+ const name = f(n, "name");
191
+ return name ? { name: lastSeg(name.text), kind: n.type === "module" ? "module" : "class" } : null;
192
+ },
193
+ func: (n) => (n.type === "method" || n.type === "singleton_method") && nameOf(n) ? { name: nameOf(n) } : null,
194
+ call: (n, self) => {
195
+ if (n.type !== "call")
196
+ return null;
197
+ const name = f(n, "method")?.text;
198
+ if (!name)
199
+ return null;
200
+ const recv = f(n, "receiver");
201
+ if (!recv && RUBY_SKIP.has(name))
202
+ return null;
203
+ return { name, obj: objOf(recv, self) };
204
+ },
205
+ imports: (n, acc) => {
206
+ if (n.type !== "call" || f(n, "receiver"))
207
+ return;
208
+ const m = f(n, "method")?.text;
209
+ if (m !== "require" && m !== "require_relative" && m !== "load")
210
+ return;
211
+ const arg = f(n, "arguments")?.namedChild(0);
212
+ const spec = arg?.type === "string" ? arg.namedChildren.find((c) => c.type === "string_content")?.text : null;
213
+ if (spec)
214
+ acc.imports.push(m === "require_relative" ? (spec.startsWith(".") ? spec : "./" + spec) : spec);
215
+ },
216
+ };
217
+ // ───────────── PHP ─────────────
218
+ const php = {
219
+ implicitSelf: false,
220
+ type: ofTypes({ class_declaration: "class", interface_declaration: "interface", trait_declaration: "trait", enum_declaration: "enum" }),
221
+ func: (n) => (n.type === "method_declaration" || n.type === "function_definition") && nameOf(n) ? { name: nameOf(n) } : null,
222
+ call: (n, self) => {
223
+ switch (n.type) {
224
+ case "function_call_expression": {
225
+ const fn = f(n, "function");
226
+ return fn && (fn.type === "name" || fn.type === "qualified_name") ? { name: lastSeg(fn.text), obj: "" } : null;
227
+ }
228
+ case "member_call_expression":
229
+ case "nullsafe_member_call_expression": {
230
+ const name = nameOf(n);
231
+ return name ? { name, obj: objOf(f(n, "object"), self) } : null;
232
+ }
233
+ case "scoped_call_expression": {
234
+ const name = nameOf(n), scope = f(n, "scope");
235
+ if (!name || !scope)
236
+ return null;
237
+ return { name, obj: scope.type === "relative_scope" ? "this" : lastSeg(scope.text) };
238
+ }
239
+ }
240
+ return null;
241
+ },
242
+ imports: (n, acc) => {
243
+ if (n.type !== "namespace_use_declaration")
244
+ return;
245
+ const add = (spec, alias) => {
246
+ spec = spec.replace(/^\\/, "");
247
+ acc.imports.push(spec);
248
+ acc.bindings.push([alias ?? lastSeg(spec), spec, lastSeg(spec)]);
249
+ };
250
+ const aliasOf = (c) => c.namedChildren.find((x) => x.type === "namespace_aliasing_clause")?.namedChild(0)?.text;
251
+ const group = n.namedChildren.find((c) => c.type === "namespace_use_group");
252
+ if (group) {
253
+ const prefix = n.namedChildren.find((c) => c.type === "namespace_name")?.text.replace(/\\$/, "") ?? "";
254
+ for (const c of group.namedChildren) {
255
+ const nm = c.namedChildren.find((x) => x.type === "namespace_name" || x.type === "name" || x.type === "qualified_name");
256
+ if (nm)
257
+ add(`${prefix}\\${nm.text}`, aliasOf(c));
258
+ }
259
+ return;
260
+ }
261
+ for (const c of n.namedChildren.filter((x) => x.type === "namespace_use_clause")) {
262
+ const q = c.namedChildren.find((x) => x.type === "qualified_name" || x.type === "name");
263
+ if (q)
264
+ add(q.text, aliasOf(c));
265
+ }
266
+ },
267
+ };
268
+ // ───────────── C# ─────────────
269
+ const csharp = {
270
+ implicitSelf: true,
271
+ type: ofTypes({ class_declaration: "class", interface_declaration: "interface", struct_declaration: "struct", record_declaration: "class", record_struct_declaration: "struct", enum_declaration: "enum" }),
272
+ func: (n) => (n.type === "method_declaration" || n.type === "constructor_declaration" || n.type === "local_function_statement") && nameOf(n) ? { name: nameOf(n) } : null,
273
+ call: (n, self) => {
274
+ if (n.type !== "invocation_expression")
275
+ return null;
276
+ const fn = f(n, "function");
277
+ if (!fn)
278
+ return null;
279
+ if (fn.type === "identifier")
280
+ return { name: fn.text, obj: "" };
281
+ if (fn.type === "generic_name")
282
+ return { name: fn.namedChild(0)?.text ?? fn.text, obj: "" };
283
+ const m = member(fn);
284
+ return m ? { name: m.name.replace(/<.*$/, ""), obj: objOf(m.base, self) } : null;
285
+ },
286
+ imports: (n, acc) => {
287
+ if (n.type !== "using_directive")
288
+ return;
289
+ const id = n.namedChildren.filter((c) => c.type === "identifier" || c.type === "qualified_name").pop();
290
+ if (id && !n.namedChildren.some((c) => c.type === "name_equals"))
291
+ acc.imports.push(id.text);
292
+ },
293
+ };
294
+ // ───────────── Rust ─────────────
295
+ function rustTypeName(t) {
296
+ for (let i = 0; t && i < 6; i++) {
297
+ if (t.type === "type_identifier")
298
+ return t.text;
299
+ t = f(t, "type") ?? f(t, "name") ?? t.namedChild(0);
300
+ }
301
+ return null;
302
+ }
303
+ function rustUses(a, prefix, out) {
304
+ switch (a.type) {
305
+ case "scoped_use_list": {
306
+ const p = prefix.concat(f(a, "path")?.text.split("::") ?? []);
307
+ for (const c of f(a, "list")?.namedChildren ?? [])
308
+ rustUses(c, p, out);
309
+ break;
310
+ }
311
+ case "use_as_clause":
312
+ out.push({ path: prefix.concat(f(a, "path")?.text.split("::") ?? []), alias: f(a, "alias")?.text });
313
+ break;
314
+ case "use_wildcard":
315
+ out.push({ path: prefix.concat(a.text.replace(/\*$/, "").split("::").filter(Boolean)), wildcard: true });
316
+ break;
317
+ case "identifier":
318
+ case "scoped_identifier":
319
+ case "self":
320
+ case "crate":
321
+ out.push({ path: prefix.concat(a.text.split("::")) });
322
+ break;
323
+ }
324
+ }
325
+ const rust = {
326
+ implicitSelf: false,
327
+ type: ofTypes({ struct_item: "struct", enum_item: "enum", trait_item: "trait", union_item: "struct" }),
328
+ scope: (n) => (n.type === "impl_item" ? rustTypeName(f(n, "type")) : null),
329
+ func: (n) => (n.type === "function_item" || n.type === "function_signature_item") && nameOf(n) ? { name: nameOf(n) } : null,
330
+ call: (n, self) => {
331
+ if (n.type !== "call_expression")
332
+ return null;
333
+ let fn = f(n, "function");
334
+ if (fn?.type === "generic_function")
335
+ fn = f(fn, "function");
336
+ if (!fn)
337
+ return null;
338
+ if (fn.type === "identifier")
339
+ return { name: fn.text, obj: "" };
340
+ if (fn.type === "scoped_identifier") {
341
+ const name = nameOf(fn), owner = lastSeg(f(fn, "path")?.text ?? "");
342
+ if (!name)
343
+ return null;
344
+ // Tipo::f() é método associado; modulo::f() é função solta.
345
+ return { name, obj: owner === "Self" ? "this" : /^[A-Z]/.test(owner) ? owner : "" };
346
+ }
347
+ const m = member(fn);
348
+ return m ? { name: m.name, obj: objOf(m.base, self) } : null;
349
+ },
350
+ imports: (n, acc) => {
351
+ if (n.type !== "use_declaration")
352
+ return;
353
+ const arg = f(n, "argument");
354
+ if (!arg)
355
+ return;
356
+ const uses = [];
357
+ rustUses(arg, [], uses);
358
+ for (const u of uses) {
359
+ if (u.path[u.path.length - 1] === "self")
360
+ u.path.pop();
361
+ if (!u.path.length)
362
+ continue;
363
+ const spec = u.path.join("::");
364
+ acc.imports.push(spec);
365
+ if (!u.wildcard)
366
+ acc.bindings.push([u.alias ?? u.path[u.path.length - 1], spec, u.path[u.path.length - 1]]);
367
+ }
368
+ },
369
+ };
370
+ const HOOKS = { java, kotlin, go, ruby, php, c_sharp: csharp, rust };
371
+ export function extractLang(root, lang, file) {
372
+ const h = HOOKS[lang];
373
+ const syms = [], calls = [];
374
+ const acc = { imports: [], bindings: [] };
375
+ const mk = (name, kind, n) => {
376
+ const s = { id: `${file}::${name}:${n.startPosition.row + 1}`, name, kind, file, startLine: n.startPosition.row + 1, endLine: n.endPosition.row + 1 };
377
+ syms.push(s);
378
+ return s;
379
+ };
380
+ const walk = (n, owner, cls, self) => {
381
+ let nextOwner = owner, nextCls = cls, nextSelf = self;
382
+ const t = h.type?.(n);
383
+ if (t) {
384
+ nextOwner = mk(t.name, t.kind, n);
385
+ nextCls = t.name;
386
+ }
387
+ else {
388
+ const sc = h.scope?.(n);
389
+ if (sc)
390
+ nextCls = sc;
391
+ const fn = h.func?.(n);
392
+ if (fn) {
393
+ const c = fn.cls ?? cls;
394
+ nextOwner = mk(c ? `${c}.${fn.name}` : fn.name, c ? "method" : "function", n);
395
+ if (fn.cls)
396
+ nextCls = fn.cls;
397
+ if (fn.self !== undefined)
398
+ nextSelf = fn.self;
399
+ }
400
+ }
401
+ const c = h.call?.(n, nextSelf);
402
+ if (c && nextOwner)
403
+ calls.push({ callerId: nextOwner.id, callee: c.name, obj: !c.obj && h.implicitSelf && nextCls ? "@" : c.obj, file });
404
+ h.imports?.(n, acc);
405
+ for (const ch of n.namedChildren)
406
+ walk(ch, nextOwner, nextCls, nextSelf);
407
+ };
408
+ walk(root, null, null, null);
409
+ return { syms, calls, imports: acc.imports, bindings: acc.bindings };
410
+ }
411
+ const FAMILY = { ".java": "jvm", ".kt": "jvm", ".go": "go", ".rb": "ruby", ".php": "php", ".cs": "cs", ".rs": "rust" };
412
+ const MAX_TARGETS = 40;
413
+ /**
414
+ * Nessas linguagens o import nomeia um pacote/namespace/módulo, não um caminho. O índice casa o especificador com o
415
+ * final do caminho dos arquivos do projeto (com.acme.Repo ↔ src/main/java/com/acme/Repo.java), sem depender de
416
+ * convenção de build. Pacote externo (java.util, serde, github.com/gin-gonic/gin) não casa com nada e volta [].
417
+ */
418
+ export class ProjectIndex {
419
+ root;
420
+ known;
421
+ files = new Map();
422
+ dirs = new Map();
423
+ dirsFull = new Map();
424
+ goModule = null;
425
+ constructor(root, known) {
426
+ this.root = root;
427
+ this.known = known;
428
+ for (const file of known) {
429
+ const ext = path.posix.extname(file);
430
+ if (!FAMILY[ext])
431
+ continue;
432
+ let base = file.slice(0, -ext.length).toLowerCase();
433
+ if (ext === ".rs" && base.endsWith("/mod"))
434
+ base = base.slice(0, -4);
435
+ this.addSuffixes(this.files, base, file);
436
+ const dir = path.posix.dirname(file).toLowerCase();
437
+ if (dir !== ".")
438
+ this.addSuffixes(this.dirs, dir, file);
439
+ (this.dirsFull.get(dir) ?? this.dirsFull.set(dir, new Set()).get(dir)).add(file);
440
+ }
441
+ try {
442
+ this.goModule = fs.readFileSync(path.join(root, "go.mod"), "utf8").match(/^module\s+(\S+)/m)?.[1] ?? null;
443
+ }
444
+ catch { /* sem go.mod */ }
445
+ }
446
+ addSuffixes(map, p, file) {
447
+ const segs = p.split("/");
448
+ for (let i = 0; i < segs.length; i++) {
449
+ const k = segs.slice(i).join("/");
450
+ (map.get(k) ?? map.set(k, new Set()).get(k)).add(file);
451
+ }
452
+ }
453
+ /** Tenta o especificador inteiro e depois sem os últimos (membro/classe aninhada) e os primeiros (prefixo raiz) segmentos. */
454
+ find(map, parts, fam, p) {
455
+ const minLen = p.minLen ?? (parts.length >= 2 ? 2 : 1);
456
+ const keys = parts.map((x) => x.toLowerCase());
457
+ for (let t = 0; t <= p.trail; t++) {
458
+ for (let l = 0; l <= p.lead && keys.length - t - l >= minLen; l++) {
459
+ const hit = [...(map.get(keys.slice(l, keys.length - t).join("/")) ?? [])].filter((x) => FAMILY[path.posix.extname(x)] === fam && !x.endsWith("_test.go"));
460
+ if (hit.length)
461
+ return hit;
462
+ }
463
+ }
464
+ return [];
465
+ }
466
+ /** null = o arquivo de origem não é de uma linguagem tratada aqui (quem resolve é o resolveImport de TS/Python). */
467
+ resolve(from, spec) {
468
+ const fam = FAMILY[path.posix.extname(from)];
469
+ if (!fam)
470
+ return null;
471
+ let hits = [];
472
+ switch (fam) {
473
+ case "jvm": {
474
+ const parts = spec.split(".");
475
+ hits = this.find(this.files, parts, fam, { trail: 2, lead: 0 });
476
+ if (!hits.length)
477
+ hits = this.find(this.dirs, parts, fam, { trail: 1, lead: 0 });
478
+ break;
479
+ }
480
+ case "php":
481
+ hits = this.find(this.files, spec.replace(/^\\/, "").split("\\"), fam, { trail: 0, lead: 3 });
482
+ break;
483
+ case "cs": {
484
+ const parts = spec.split(".");
485
+ hits = this.find(this.dirs, parts, fam, { trail: 0, lead: 2 });
486
+ if (!hits.length)
487
+ hits = this.find(this.files, parts, fam, { trail: 1, lead: 2 });
488
+ break;
489
+ }
490
+ case "rust": {
491
+ const parts = spec.split("::");
492
+ while (["crate", "self", "super"].includes(parts[0]))
493
+ parts.shift();
494
+ if (!parts.length || ["std", "core", "alloc"].includes(parts[0]))
495
+ break;
496
+ hits = this.find(this.files, parts, fam, { trail: 2, lead: 0, minLen: 1 });
497
+ break;
498
+ }
499
+ case "ruby": {
500
+ if (spec.startsWith(".")) {
501
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(from), spec)).replace(/\.rb$/, "") + ".rb";
502
+ hits = this.known.has(base) ? [base] : [];
503
+ }
504
+ else
505
+ hits = this.find(this.files, spec.replace(/\.rb$/, "").split("/"), fam, { trail: 0, lead: 0 });
506
+ break;
507
+ }
508
+ case "go": {
509
+ const m = this.goModule;
510
+ if (m && (spec === m || spec.startsWith(m + "/"))) {
511
+ const rel = (spec === m ? "." : spec.slice(m.length + 1)).toLowerCase();
512
+ hits = [...(this.dirsFull.get(rel) ?? [])].filter((x) => !x.endsWith("_test.go") && x.endsWith(".go"));
513
+ if (!hits.length)
514
+ hits = this.find(this.dirs, rel.split("/"), fam, { trail: 0, lead: 0, minLen: 1 });
515
+ }
516
+ else if (!m)
517
+ hits = this.find(this.dirs, spec.split("/"), fam, { trail: 0, lead: 4 });
518
+ break;
519
+ }
520
+ }
521
+ return hits.slice(0, MAX_TARGETS);
522
+ }
523
+ }
524
+ // Para suportar outra linguagem (Scala, Swift, Elixir…): 1) acrescente a extensão em EXTRA_EXT e ExtraLang (a gramática
525
+ // precisa existir em tree-sitter-wasms/out); 2) escreva os hooks (use scripts de dump da AST para achar os tipos de nó);
526
+ // 3) registre-os em HOOKS e a família de resolução em FAMILY/ProjectIndex.resolve.
@@ -0,0 +1,52 @@
1
+ // Componentes de arquivo único (.svelte, .vue, .astro): para o parser só o(s) bloco(s) de script é código;
2
+ // o resto é template e estilo. Tudo fora do script vira espaço (as quebras de linha ficam), então a linha de
3
+ // cada símbolo no grafo é a linha real do arquivo.
4
+ export const SFC_EXT = new Set([".svelte", ".vue", ".astro"]);
5
+ const blank = (s) => s.replace(/[^\r\n]/g, " ");
6
+ /** `<script type="application/json">`, `text/template`… não são código. */
7
+ const JS_TYPES = /^(module|(text|application)\/(javascript|typescript|ecmascript))$/;
8
+ export function splitSfc(src, ext) {
9
+ const ranges = [];
10
+ let tsx = false;
11
+ for (const m of src.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi)) {
12
+ const attrs = m[1] ?? "";
13
+ const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(attrs)?.[1]?.toLowerCase();
14
+ if (type && !JS_TYPES.test(type))
15
+ continue;
16
+ if (/\blang\s*=\s*["']?tsx\b/i.test(attrs))
17
+ tsx = true;
18
+ const start = m.index + m[0].indexOf(">") + 1;
19
+ ranges.push([start, start + m[2].length]);
20
+ }
21
+ // Astro: o código do servidor fica no frontmatter (--- … ---) no topo do arquivo.
22
+ if (ext === ".astro") {
23
+ const fm = /^(\s*---[ \t]*\r?\n)([\s\S]*?)(\r?\n---)/.exec(src);
24
+ if (fm)
25
+ ranges.push([fm[1].length, fm[1].length + fm[2].length]);
26
+ }
27
+ ranges.sort((a, b) => a[0] - b[0]);
28
+ let script = "", template = "", pos = 0;
29
+ for (const [s, e] of ranges) {
30
+ if (s < pos)
31
+ continue; // sobreposição (ex.: "<script>" citado dentro do frontmatter)
32
+ script += blank(src.slice(pos, s)) + src.slice(s, e);
33
+ template += src.slice(pos, s) + blank(src.slice(s, e));
34
+ pos = e;
35
+ }
36
+ script += blank(src.slice(pos));
37
+ template += src.slice(pos);
38
+ template = template.replace(/<style\b[\s\S]*?<\/style\s*>|<!--[\s\S]*?-->/gi, blank);
39
+ return { script, template, tsx };
40
+ }
41
+ /**
42
+ * Chamadas feitas no markup (`{formatar(x)}`, `onclick={() => salvar()}`). Só conta nome que o script declara
43
+ * ou importa (`known`): assim `class="a (b)"` ou texto solto não viram aresta, e nada cai na heurística por nome.
44
+ */
45
+ export function templateCalleeNames(template, known) {
46
+ const out = new Set();
47
+ for (const m of template.matchAll(/(?<![\w$.#])([A-Za-z_$][\w$]*)\s*\(/g))
48
+ if (known.has(m[1]))
49
+ out.add(m[1]);
50
+ return [...out];
51
+ }
52
+ export const componentName = (rel) => rel.slice(rel.lastIndexOf("/") + 1).replace(/\.[^.]+$/, "");
@@ -0,0 +1,96 @@
1
+ import kuzu from "kuzu";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ /** Lê as linhas e fecha cada QueryResult na hora: o finalizer do GC depois de db.close() derruba o processo (segfault). */
5
+ async function drain(r, read = true) {
6
+ const all = Array.isArray(r) ? r : [r];
7
+ try {
8
+ return read ? await all[all.length - 1].getAll() : [];
9
+ }
10
+ finally {
11
+ for (const x of all)
12
+ x.close();
13
+ }
14
+ }
15
+ /** Outro processo do Brito está com o banco aberto: o Kùzu só deixa um processo por projeto. */
16
+ export class GraphLockedError extends Error {
17
+ constructor(file) {
18
+ super(`O grafo deste projeto está em uso por outro processo do Brito (${file}).\n` +
19
+ "O banco aceita um processo por vez: provavelmente há outro terminal com o Brito, ou uma sessão do Claude Code usando o Brito, neste mesmo diretório.\n" +
20
+ "Feche o outro, ou espere o turno em andamento terminar, e tente de novo.");
21
+ this.name = "GraphLockedError";
22
+ }
23
+ }
24
+ const isLockError = (e) => /could not set lock/i.test(String(e?.message ?? e));
25
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
+ /** Quanto esperar o banco liberar antes de desistir: cobre sobreposições curtas (a reflexão de skills e o turno, por exemplo). */
27
+ const LOCK_WAIT_MS = 4000;
28
+ export class GraphStore {
29
+ db;
30
+ conn;
31
+ queue = Promise.resolve();
32
+ constructor(db, conn) {
33
+ this.db = db;
34
+ this.conn = conn;
35
+ }
36
+ static async open(root, waitMs = LOCK_WAIT_MS) {
37
+ const dir = path.join(root, ".brito");
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ const file = path.join(dir, "graph.kuzu");
40
+ const deadline = Date.now() + waitMs;
41
+ for (;;) {
42
+ try {
43
+ const db = new kuzu.Database(file);
44
+ const store = new GraphStore(db, new kuzu.Connection(db));
45
+ await store.migrate();
46
+ return store;
47
+ }
48
+ catch (e) {
49
+ if (!isLockError(e))
50
+ throw e;
51
+ if (Date.now() >= deadline)
52
+ throw new GraphLockedError(file);
53
+ await sleep(250);
54
+ }
55
+ }
56
+ }
57
+ async migrate() {
58
+ const ddl = [
59
+ "CREATE NODE TABLE IF NOT EXISTS File(path STRING, lang STRING, hash STRING, calls STRING[], imports STRING[], bindings STRING[], PRIMARY KEY(path))",
60
+ "CREATE NODE TABLE IF NOT EXISTS Symbol(id STRING, name STRING, kind STRING, file STRING, startLine INT64, endLine INT64, PRIMARY KEY(id))",
61
+ "CREATE NODE TABLE IF NOT EXISTS Note(id STRING, kind STRING, text STRING, refs STRING[], ts INT64, PRIMARY KEY(id))",
62
+ "CREATE REL TABLE IF NOT EXISTS DEFINES(FROM File TO Symbol)",
63
+ "CREATE REL TABLE IF NOT EXISTS CALLS(FROM Symbol TO Symbol)",
64
+ "CREATE REL TABLE IF NOT EXISTS IMPORTS(FROM File TO File)",
65
+ ];
66
+ for (const q of ddl)
67
+ await drain(await this.conn.query(q), false);
68
+ // Bancos criados antes do indexador incremental não têm estas colunas.
69
+ for (const col of ["calls", "imports", "bindings"])
70
+ await drain(await this.conn.query(`ALTER TABLE File ADD IF NOT EXISTS ${col} STRING[] DEFAULT []`), false);
71
+ }
72
+ /** Serializa as queries: leituras paralelas de subagentes dividem uma única Connection. */
73
+ run(cypher, params = {}) {
74
+ const next = this.queue.then(() => this.exec(cypher, params));
75
+ this.queue = next.catch(() => undefined);
76
+ return next;
77
+ }
78
+ async exec(cypher, params) {
79
+ if (Object.keys(params).length === 0) {
80
+ return drain(await this.conn.query(cypher));
81
+ }
82
+ const ps = await this.conn.prepare(cypher);
83
+ if (!ps.isSuccess())
84
+ throw new Error(ps.getErrorMessage());
85
+ return drain(await this.conn.execute(ps, params));
86
+ }
87
+ /** Apaga só o grafo de código; notas (memória) são preservadas. */
88
+ async clearCode() {
89
+ await this.run("MATCH (s:Symbol) DETACH DELETE s");
90
+ await this.run("MATCH (f:File) DETACH DELETE f");
91
+ }
92
+ async close() {
93
+ await this.conn.close();
94
+ await this.db.close();
95
+ }
96
+ }