@esneiderbravo/speclaw 2.0.9 → 2.0.11
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/README.md +6 -2
- package/dist/cli/commands/query.js +12 -2
- package/dist/cli/commands/reindex-file.js +146 -0
- package/dist/cli/commands/update.js +32 -0
- package/dist/cli/index.js +7 -3
- package/dist/cli/lib/help.js +19 -0
- package/dist/modules/compass/affected-config.js +5 -0
- package/dist/modules/compass/affected.js +344 -27
- package/dist/modules/compass/db.js +149 -16
- package/dist/modules/compass/diff-context.js +4 -2
- package/dist/modules/compass/explore-rich.js +14 -7
- package/dist/modules/compass/extract.js +163 -4
- package/dist/modules/compass/hotspots.js +11 -6
- package/dist/modules/compass/hybrid.js +9 -4
- package/dist/modules/compass/indexer.js +898 -202
- package/dist/modules/compass/map.js +2 -2
- package/dist/modules/compass/query.js +70 -24
- package/dist/modules/compass/register.js +1 -1
- package/dist/modules/compass/visualize.js +5 -3
- package/dist/modules/foundation/assets/docs/compass.template.md +22 -2
- package/dist/modules/foundation/hooks.js +48 -10
- package/dist/modules/lawbook/drift.js +2 -2
- package/dist/modules/lawbook/investigate.js +2 -1
- package/dist/shared/output-budget.js +2 -0
- package/dist/shared/paths.js +24 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isGitRepo, changedFiles, worktreeChangedFiles } from "../../shared/git.js";
|
|
2
|
-
import { openDb, indexExists } from "./db.js";
|
|
2
|
+
import { openDb, indexExists, FILE_NODE_KIND } from "./db.js";
|
|
3
3
|
import { impact } from "./query.js";
|
|
4
4
|
import { affectedTests } from "./affected.js";
|
|
5
5
|
import { hotspots } from "./hotspots.js";
|
|
@@ -26,7 +26,7 @@ function symbolsForFiles(projectPath, files, overestimate) {
|
|
|
26
26
|
const nodes = db
|
|
27
27
|
.prepare(`SELECT n.name, n.kind, n.start_line AS line, f.path AS file
|
|
28
28
|
FROM nodes n JOIN files f ON f.id = n.file_id
|
|
29
|
-
WHERE f.path =
|
|
29
|
+
WHERE f.path = ? AND n.kind <> '${FILE_NODE_KIND}'`)
|
|
30
30
|
.all(file);
|
|
31
31
|
if (overestimate || nodes.length === 0) {
|
|
32
32
|
for (const n of nodes)
|
|
@@ -100,6 +100,8 @@ export function diffContext(query) {
|
|
|
100
100
|
result.affectedTests = {
|
|
101
101
|
tests: at.tests,
|
|
102
102
|
command: at.command,
|
|
103
|
+
commandReason: at.commandReason,
|
|
104
|
+
commands: at.commands,
|
|
103
105
|
mode: at.mode,
|
|
104
106
|
reason: at.reason,
|
|
105
107
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { explore, impact, trace } from "./query.js";
|
|
2
2
|
import { affectedTests } from "./affected.js";
|
|
3
|
+
import { FILE_NODE_KIND } from "./db.js";
|
|
3
4
|
import { hotspots } from "./hotspots.js";
|
|
4
5
|
import { summarizeImpact } from "./impact-summary.js";
|
|
5
6
|
import { budgetExploreShape, applyTextBudget, } from "../../shared/output-budget.js";
|
|
@@ -41,8 +42,10 @@ export async function exploreRich(query) {
|
|
|
41
42
|
out.symbol = withoutSource(out.symbol);
|
|
42
43
|
if (!includes.includes("callers"))
|
|
43
44
|
out.callers = [];
|
|
44
|
-
if (!includes.includes("callees"))
|
|
45
|
+
if (!includes.includes("callees")) {
|
|
45
46
|
out.callees = [];
|
|
47
|
+
delete out.unresolvedCallees;
|
|
48
|
+
}
|
|
46
49
|
budgetExploreShape(out, mode, truncated);
|
|
47
50
|
return out;
|
|
48
51
|
}
|
|
@@ -52,15 +55,17 @@ export async function exploreRich(query) {
|
|
|
52
55
|
out.symbol = withoutSource(out.symbol);
|
|
53
56
|
if (!includes.includes("callers"))
|
|
54
57
|
out.callers = [];
|
|
55
|
-
if (!includes.includes("callees"))
|
|
58
|
+
if (!includes.includes("callees")) {
|
|
56
59
|
out.callees = [];
|
|
60
|
+
delete out.unresolvedCallees;
|
|
61
|
+
}
|
|
57
62
|
if (base.found && base.symbol) {
|
|
58
63
|
const sym = base.symbol.name;
|
|
59
64
|
const file = base.symbol.file;
|
|
60
65
|
if (includes.includes("blast_radius")) {
|
|
61
66
|
try {
|
|
62
67
|
const imp = impact(query.projectPath, {
|
|
63
|
-
symbol: sym,
|
|
68
|
+
...(base.symbol.kind === FILE_NODE_KIND ? { files: [file] } : { symbol: sym }),
|
|
64
69
|
maxDepth: query.maxDepth ?? 4,
|
|
65
70
|
format: "grouped",
|
|
66
71
|
});
|
|
@@ -72,14 +77,16 @@ export async function exploreRich(query) {
|
|
|
72
77
|
}
|
|
73
78
|
if (includes.includes("tests")) {
|
|
74
79
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
maxDepth: query.maxDepth ?? 6
|
|
78
|
-
|
|
80
|
+
// A file-owner node is named by its path: select by file, not by name.
|
|
81
|
+
const at = affectedTests(query.projectPath, base.symbol.kind === FILE_NODE_KIND
|
|
82
|
+
? { files: [file], maxDepth: query.maxDepth ?? 6 }
|
|
83
|
+
: { symbols: [sym], maxDepth: query.maxDepth ?? 6 });
|
|
79
84
|
out.affectedTests = {
|
|
80
85
|
count: at.tests.length,
|
|
81
86
|
files: at.tests.map((t) => t.file),
|
|
82
87
|
command: at.command,
|
|
88
|
+
commandReason: at.commandReason,
|
|
89
|
+
commands: at.commands,
|
|
83
90
|
};
|
|
84
91
|
}
|
|
85
92
|
catch {
|
|
@@ -34,6 +34,115 @@ function calleeName(node, lang) {
|
|
|
34
34
|
return fn.text;
|
|
35
35
|
return fn.text.split(/[.\s(]/)[0] || null;
|
|
36
36
|
}
|
|
37
|
+
/** Python receivers that mean "this class/instance". */
|
|
38
|
+
const PY_SELF = new Set(["self", "cls"]);
|
|
39
|
+
/**
|
|
40
|
+
* The receiver of a member call (`recv.f()`), or null for a plain call.
|
|
41
|
+
* `kind` is `self` for `this`/`super`/`self`/`cls`, `ident` for a bare
|
|
42
|
+
* identifier (classified later against the file's import bindings), and
|
|
43
|
+
* `other` for any chain, subscript, or call result.
|
|
44
|
+
*/
|
|
45
|
+
function callReceiver(node, lang) {
|
|
46
|
+
const fn = node.childForFieldName(lang.callField);
|
|
47
|
+
if (!fn)
|
|
48
|
+
return null;
|
|
49
|
+
if (fn.type === "identifier" || fn.type === "super")
|
|
50
|
+
return null;
|
|
51
|
+
if (fn.type !== "member_expression" && fn.type !== "attribute") {
|
|
52
|
+
return { kind: "other", text: fn.text };
|
|
53
|
+
}
|
|
54
|
+
const obj = fn.childForFieldName("object");
|
|
55
|
+
if (!obj)
|
|
56
|
+
return { kind: "other", text: fn.text };
|
|
57
|
+
if (obj.type === "this" || obj.type === "super")
|
|
58
|
+
return { kind: "self", text: obj.text };
|
|
59
|
+
if (obj.type === "identifier") {
|
|
60
|
+
// Python's `self`/`cls` are conventions, not keywords; JS `self` is a global.
|
|
61
|
+
if (lang.id === "python" && PY_SELF.has(obj.text))
|
|
62
|
+
return { kind: "self", text: obj.text };
|
|
63
|
+
return { kind: "ident", text: obj.text };
|
|
64
|
+
}
|
|
65
|
+
return { kind: "other", text: obj.text };
|
|
66
|
+
}
|
|
67
|
+
/** Longest import text stored on an edge (`dst_name`). */
|
|
68
|
+
export const IMPORT_TEXT_CAP = 1024;
|
|
69
|
+
/**
|
|
70
|
+
* Module specifier of a raw import statement (`from "x"`, `import "x"`,
|
|
71
|
+
* `require("x")`, `import("x")`), or null when the text has none.
|
|
72
|
+
*
|
|
73
|
+
* @param text - Import statement text, whitespace collapsed.
|
|
74
|
+
*/
|
|
75
|
+
export function importSpecifier(text) {
|
|
76
|
+
const m = text.match(/\bfrom\s+['"]([^'"]+)['"]/) ??
|
|
77
|
+
text.match(/^import\s+['"]([^'"]+)['"]/) ??
|
|
78
|
+
text.match(/\brequire\s*\(\s*['"]([^'"]+)['"]/) ??
|
|
79
|
+
text.match(/\bimport\s*\(\s*['"]([^'"]+)['"]/);
|
|
80
|
+
return m ? m[1] : null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Import text as stored on its edge: unchanged up to {@link IMPORT_TEXT_CAP}
|
|
84
|
+
* characters, else the head of the statement, ` … `, and its trailing `from`
|
|
85
|
+
* clause, so the specifier at the end survives the cap. Bindings and the
|
|
86
|
+
* specifier are read from the full text before capping; this only bounds what
|
|
87
|
+
* is stored.
|
|
88
|
+
*
|
|
89
|
+
* @param text - Import statement text, whitespace collapsed.
|
|
90
|
+
*/
|
|
91
|
+
export function capImportText(text) {
|
|
92
|
+
if (text.length <= IMPORT_TEXT_CAP)
|
|
93
|
+
return text;
|
|
94
|
+
const tail = text.match(/\s(from\s+['"][^'"]+['"].*)$/)?.[1] ??
|
|
95
|
+
text.match(/((?:require|import)\s*\(\s*['"][^'"]+['"].*)$/)?.[1];
|
|
96
|
+
if (!tail || tail.length > IMPORT_TEXT_CAP / 2)
|
|
97
|
+
return text.slice(0, IMPORT_TEXT_CAP);
|
|
98
|
+
return `${text.slice(0, IMPORT_TEXT_CAP - tail.length - 3)} … ${tail}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Local names an import statement binds: default, namespace (`* as ns`), and
|
|
102
|
+
* named (`{ a, b as c }` → `a`, `c`) for JS/TS; `import a.b` → `a`,
|
|
103
|
+
* `import x as y` → `y`, `from m import a, b as c` → `a`, `c` for Python.
|
|
104
|
+
*
|
|
105
|
+
* @param text - Import statement text, whitespace collapsed.
|
|
106
|
+
* @param langId - Language id of the file.
|
|
107
|
+
*/
|
|
108
|
+
export function importBindings(text, langId) {
|
|
109
|
+
const out = [];
|
|
110
|
+
const ident = /^[A-Za-z_$][\w$]*$/;
|
|
111
|
+
const localOf = (part) => {
|
|
112
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
113
|
+
const name = (bits[1] ?? bits[0] ?? "").trim().replace(/^type\s+/, "");
|
|
114
|
+
return ident.test(name) ? name : null;
|
|
115
|
+
};
|
|
116
|
+
if (langId === "python") {
|
|
117
|
+
const from = text.match(/^from\s+\S+\s+import\s+(.+)$/);
|
|
118
|
+
const list = from ? from[1] : (text.match(/^import\s+(.+)$/)?.[1] ?? "");
|
|
119
|
+
for (const raw of list.replace(/[()]/g, "").split(",")) {
|
|
120
|
+
const bits = raw.trim().split(/\s+as\s+/);
|
|
121
|
+
const name = bits[1] ?? (from ? bits[0] : bits[0]?.split(".")[0]);
|
|
122
|
+
if (name && ident.test(name.trim()))
|
|
123
|
+
out.push(name.trim());
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
const clause = text.match(/^import\s+(?:type\s+)?(.+?)\s+from\s/)?.[1];
|
|
128
|
+
if (!clause)
|
|
129
|
+
return out;
|
|
130
|
+
const ns = clause.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
|
|
131
|
+
if (ns)
|
|
132
|
+
out.push(ns[1]);
|
|
133
|
+
const brace = clause.match(/\{([^}]*)\}/);
|
|
134
|
+
if (brace) {
|
|
135
|
+
for (const part of brace[1].split(",")) {
|
|
136
|
+
const name = localOf(part);
|
|
137
|
+
if (name)
|
|
138
|
+
out.push(name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const def = clause.match(/^([A-Za-z_$][\w$]*)\s*(?:,|$)/);
|
|
142
|
+
if (def)
|
|
143
|
+
out.push(def[1]);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
37
146
|
/** First line of the node's text, trimmed — a lightweight signature. */
|
|
38
147
|
function signatureOf(node) {
|
|
39
148
|
return node.text.split("\n")[0].trim().slice(0, 200);
|
|
@@ -223,6 +332,17 @@ export async function extract(source, lang) {
|
|
|
223
332
|
const symbols = [];
|
|
224
333
|
const refs = [];
|
|
225
334
|
const rawCoverage = [];
|
|
335
|
+
// Identifier receivers and bare callees are classified after the walk, once
|
|
336
|
+
// every import binding of the file is known (imports may follow their first
|
|
337
|
+
// use). Whether a binding is a package is not decided here: that needs the
|
|
338
|
+
// project's alias/baseUrl config, so resolveEdges decides it from whether the
|
|
339
|
+
// binding's import (matched by `spec`) resolved to a project file.
|
|
340
|
+
const identReceivers = [];
|
|
341
|
+
const bareCalls = [];
|
|
342
|
+
const bindings = new Set();
|
|
343
|
+
// JS/TS import binding → its import's module specifier.
|
|
344
|
+
const bindingSpecs = new Map();
|
|
345
|
+
const reexports = [];
|
|
226
346
|
const walk = (node, ownerIndex) => {
|
|
227
347
|
let nextOwner = ownerIndex;
|
|
228
348
|
if (kinds.has(node.type)) {
|
|
@@ -252,20 +372,52 @@ export async function extract(source, lang) {
|
|
|
252
372
|
}
|
|
253
373
|
else if (node.type === lang.callNode) {
|
|
254
374
|
const name = calleeName(node, lang);
|
|
255
|
-
if (name)
|
|
256
|
-
|
|
375
|
+
if (name) {
|
|
376
|
+
const receiver = callReceiver(node, lang);
|
|
377
|
+
const ref = {
|
|
378
|
+
name,
|
|
379
|
+
kind: "call",
|
|
380
|
+
line: node.startPosition.row + 1,
|
|
381
|
+
ownerIndex,
|
|
382
|
+
member: receiver !== null && receiver.kind === "other" ? 1 : 0,
|
|
383
|
+
spec: null,
|
|
384
|
+
};
|
|
385
|
+
if (receiver?.kind === "ident")
|
|
386
|
+
identReceivers.push({ ref, receiver: receiver.text });
|
|
387
|
+
else if (receiver === null)
|
|
388
|
+
bareCalls.push(ref);
|
|
389
|
+
refs.push(ref);
|
|
390
|
+
}
|
|
257
391
|
}
|
|
258
392
|
else if (importSet.has(node.type)) {
|
|
393
|
+
// Covers: req~import-resolution~1
|
|
394
|
+
// The whole statement, so a specifier on a later line is still parseable.
|
|
395
|
+
// Bindings and the specifier come from the full text; only storage is capped.
|
|
396
|
+
const text = node.text.replace(/\s+/g, " ").trim();
|
|
397
|
+
const bound = importBindings(text, lang.id);
|
|
398
|
+
const spec = lang.id === "python" ? null : importSpecifier(text);
|
|
399
|
+
for (const b of bound) {
|
|
400
|
+
bindings.add(b);
|
|
401
|
+
if (spec !== null)
|
|
402
|
+
bindingSpecs.set(b, spec);
|
|
403
|
+
}
|
|
259
404
|
refs.push({
|
|
260
|
-
name:
|
|
405
|
+
name: capImportText(text),
|
|
261
406
|
kind: "import",
|
|
262
407
|
line: node.startPosition.row + 1,
|
|
263
408
|
ownerIndex,
|
|
409
|
+
member: 0,
|
|
410
|
+
spec,
|
|
264
411
|
});
|
|
265
412
|
}
|
|
266
413
|
else if (COMMENT_TYPES.has(node.type)) {
|
|
267
414
|
rawCoverage.push(...parseCoverageComment(node, ownerIndex));
|
|
268
415
|
}
|
|
416
|
+
else if (node.type === "export_statement" && lang.id !== "python") {
|
|
417
|
+
const source = node.childForFieldName("source");
|
|
418
|
+
if (source && source.text.length >= 2)
|
|
419
|
+
reexports.push(source.text.slice(1, -1));
|
|
420
|
+
}
|
|
269
421
|
for (let i = 0; i < node.childCount; i++) {
|
|
270
422
|
const child = node.child(i);
|
|
271
423
|
if (child)
|
|
@@ -274,5 +426,12 @@ export async function extract(source, lang) {
|
|
|
274
426
|
};
|
|
275
427
|
walk(tree.rootNode, null);
|
|
276
428
|
tree.delete();
|
|
277
|
-
|
|
429
|
+
for (const { ref, receiver } of identReceivers) {
|
|
430
|
+
const spec = bindingSpecs.get(receiver) ?? null;
|
|
431
|
+
ref.member = !bindings.has(receiver) ? 1 : spec !== null ? 2 : 0;
|
|
432
|
+
ref.spec = spec;
|
|
433
|
+
}
|
|
434
|
+
for (const ref of bareCalls)
|
|
435
|
+
ref.spec = bindingSpecs.get(ref.name) ?? null;
|
|
436
|
+
return { symbols, refs, coverage: attachCoverage(rawCoverage, symbols), reexports };
|
|
278
437
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { openDb } from "./db.js";
|
|
1
|
+
import { openDb, FILE_NODE_KIND } from "./db.js";
|
|
2
|
+
import { BUILTIN_SQL_LIST } from "./indexer.js";
|
|
2
3
|
import { cachedCoChanges, cachedFileActivity } from "./git-history-cache.js";
|
|
3
4
|
import { jaccardStrength } from "../../shared/git-history.js";
|
|
4
5
|
/** Default history window for hotspot / coupling ranking. */
|
|
@@ -31,7 +32,7 @@ function loadFileHealth(projectPath) {
|
|
|
31
32
|
COALESCE(MAX(m.max_nesting), 0) AS worst_nesting,
|
|
32
33
|
COALESCE(MAX(m.branches), 0) AS worst_branches
|
|
33
34
|
FROM files f
|
|
34
|
-
LEFT JOIN nodes n ON n.file_id = f.id
|
|
35
|
+
LEFT JOIN nodes n ON n.file_id = f.id AND n.kind <> '${FILE_NODE_KIND}'
|
|
35
36
|
LEFT JOIN node_metrics m ON m.node_id = n.id
|
|
36
37
|
GROUP BY f.id`)
|
|
37
38
|
.all();
|
|
@@ -148,17 +149,21 @@ function pairInGraph(projectPath, a, b) {
|
|
|
148
149
|
.get(a, b, b, a);
|
|
149
150
|
if (row)
|
|
150
151
|
return true;
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
152
|
+
// Fallback: an unresolved edge from a whose dst_name names a symbol defined
|
|
153
|
+
// in b (or reverse). Same rule as the query-time by-name arms: member calls
|
|
154
|
+
// (is_member 1 = foreign receiver, 2 = package receiver) never match by
|
|
155
|
+
// name, and a builtin global name only matches within its own file, which
|
|
156
|
+
// a cross-file pair never is.
|
|
154
157
|
const byName = db
|
|
155
158
|
.prepare(`SELECT 1 AS ok
|
|
156
159
|
FROM edges e
|
|
157
160
|
JOIN files sf ON sf.id = e.src_file_id
|
|
158
|
-
JOIN nodes dn ON dn.name = e.dst_name
|
|
161
|
+
JOIN nodes dn ON dn.name = e.dst_name AND dn.kind <> '${FILE_NODE_KIND}'
|
|
159
162
|
JOIN files df ON df.id = dn.file_id
|
|
160
163
|
WHERE e.dst_node_id IS NULL
|
|
161
164
|
AND e.kind IN ('call', 'import')
|
|
165
|
+
AND e.is_member = 0
|
|
166
|
+
AND e.dst_name NOT IN (${BUILTIN_SQL_LIST})
|
|
162
167
|
AND ((sf.path = ? AND df.path = ?) OR (sf.path = ? AND df.path = ?))
|
|
163
168
|
LIMIT 1`)
|
|
164
169
|
.get(a, b, b, a);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Hybrid retrieval pipeline: BM25 + KNN + name → RRF → ego expand →
|
|
3
3
|
* personalized PageRank → structural rerank → token budget.
|
|
4
4
|
*/
|
|
5
|
-
import { openDb, indexExists, ftsAvailable } from "./db.js";
|
|
5
|
+
import { openDb, indexExists, ftsAvailable, FILE_NODE_KIND } from "./db.js";
|
|
6
6
|
import { getEmbedder, fromBlob, cosine } from "./embedder.js";
|
|
7
7
|
import { isGitRepo, worktreeChangedFiles } from "../../shared/git.js";
|
|
8
8
|
import { escapeFtsQuery, nameBoost, routeWeights, rrfFuse, structuralScore, MAX_DEGREE, isSymbolQuery, } from "./rank.js";
|
|
@@ -113,7 +113,7 @@ export async function hybridSearch(projectPath, query, opts = {}) {
|
|
|
113
113
|
const likeRows = db
|
|
114
114
|
.prepare(`SELECT n.id
|
|
115
115
|
FROM nodes n
|
|
116
|
-
WHERE n.name LIKE ?
|
|
116
|
+
WHERE n.name LIKE ? AND n.kind <> '${FILE_NODE_KIND}'
|
|
117
117
|
ORDER BY (n.name = ?) DESC, length(n.name) ASC
|
|
118
118
|
LIMIT 20`)
|
|
119
119
|
.all(`%${q}%`, q);
|
|
@@ -125,8 +125,11 @@ export async function hybridSearch(projectPath, query, opts = {}) {
|
|
|
125
125
|
const fused = rrfFuse({ bm25: bm25Ids, knn: knnIds, name: nameIds }, { bm25: weights.bm25, knn: weights.knn, name: weights.name });
|
|
126
126
|
// Apply name boost using node names.
|
|
127
127
|
const idMeta = new Map();
|
|
128
|
+
// File-owner nodes have no metadata here, so they never become a hit, even
|
|
129
|
+
// when the ego-graph expansion reaches one through a call edge.
|
|
128
130
|
const loadMeta = db.prepare(`SELECT n.id, n.name, n.kind, f.path AS file, n.start_line AS line, n.signature
|
|
129
|
-
FROM nodes n JOIN files f ON f.id = n.file_id
|
|
131
|
+
FROM nodes n JOIN files f ON f.id = n.file_id
|
|
132
|
+
WHERE n.id = ? AND n.kind <> '${FILE_NODE_KIND}'`);
|
|
130
133
|
for (const id of fused.keys()) {
|
|
131
134
|
const row = loadMeta.get(id);
|
|
132
135
|
if (row) {
|
|
@@ -208,7 +211,9 @@ export async function hybridSearch(projectPath, query, opts = {}) {
|
|
|
208
211
|
}
|
|
209
212
|
const prNodeIds = [...subgraphNodes, ...[...fileIdsNeeded].map((fid) => fileNodeId(fid))];
|
|
210
213
|
const defCount = new Map();
|
|
211
|
-
const allNames = db
|
|
214
|
+
const allNames = db
|
|
215
|
+
.prepare(`SELECT name FROM nodes WHERE kind <> '${FILE_NODE_KIND}'`)
|
|
216
|
+
.all();
|
|
212
217
|
for (const r of allNames)
|
|
213
218
|
defCount.set(r.name, (defCount.get(r.name) ?? 0) + 1);
|
|
214
219
|
const refCount = new Map();
|