lynkr 9.9.0 → 9.10.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/README.md +92 -23
- package/bin/cli.js +13 -7
- package/bin/lynkr-init.js +34 -12
- package/bin/lynkr-reset.js +71 -0
- package/config/model-tiers.json +199 -52
- package/package.json +3 -3
- package/scripts/build-eval-set.js +256 -0
- package/scripts/mine-difficulty-anchors.js +288 -0
- package/scripts/validate-difficulty-classifier.js +144 -0
- package/scripts/validate-intent-anchors.js +186 -0
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +292 -160
- package/src/clients/databricks.js +95 -6
- package/src/config/index.js +3 -43
- package/src/context/tool-result-compressor.js +883 -40
- package/src/orchestrator/index.js +117 -1235
- package/src/orchestrator/passthrough-stream.js +382 -0
- package/src/orchestrator/sse-transformer.js +408 -0
- package/src/routing/affinity-store.js +17 -3
- package/src/routing/classifier-setup.js +207 -0
- package/src/routing/complexity-analyzer.js +40 -4
- package/src/routing/difficulty-classifier.js +261 -0
- package/src/routing/index.js +70 -7
- package/src/routing/intent-score.js +132 -12
- package/src/routing/session-affinity.js +8 -1
- package/src/routing/side-channel-detector.js +103 -0
- package/src/server.js +20 -46
- package/src/agents/context-manager.js +0 -236
- package/src/agents/decomposition/dispatcher.js +0 -185
- package/src/agents/decomposition/gate.js +0 -136
- package/src/agents/decomposition/index.js +0 -183
- package/src/agents/decomposition/model-call.js +0 -75
- package/src/agents/decomposition/planner.js +0 -223
- package/src/agents/decomposition/synthesizer.js +0 -89
- package/src/agents/decomposition/telemetry.js +0 -55
- package/src/agents/definitions/loader.js +0 -653
- package/src/agents/executor.js +0 -457
- package/src/agents/index.js +0 -165
- package/src/agents/parallel-coordinator.js +0 -68
- package/src/agents/reflector.js +0 -331
- package/src/agents/skillbook.js +0 -331
- package/src/agents/store.js +0 -259
- package/src/edits/index.js +0 -171
- package/src/indexer/babel-parser.js +0 -213
- package/src/indexer/index.js +0 -1629
- package/src/indexer/navigation/index.js +0 -32
- package/src/indexer/navigation/providers/treeSitter.js +0 -36
- package/src/indexer/parser.js +0 -443
- package/src/tasks/store.js +0 -349
- package/src/tests/coverage.js +0 -173
- package/src/tests/index.js +0 -171
- package/src/tests/store.js +0 -213
- package/src/tools/agent-task.js +0 -145
- package/src/tools/code-mode.js +0 -304
- package/src/tools/decompose.js +0 -91
- package/src/tools/edits.js +0 -94
- package/src/tools/execution.js +0 -171
- package/src/tools/git.js +0 -1346
- package/src/tools/index.js +0 -306
- package/src/tools/indexer.js +0 -360
- package/src/tools/lazy-loader.js +0 -366
- package/src/tools/mcp-remote.js +0 -88
- package/src/tools/mcp.js +0 -116
- package/src/tools/process.js +0 -167
- package/src/tools/smart-selection.js +0 -180
- package/src/tools/stubs.js +0 -55
- package/src/tools/tasks.js +0 -260
- package/src/tools/tests.js +0 -132
- package/src/tools/tinyfish.js +0 -358
- package/src/tools/truncate.js +0 -106
- package/src/tools/web-client.js +0 -71
- package/src/tools/web.js +0 -415
- package/src/tools/workspace.js +0 -204
package/src/indexer/index.js
DELETED
|
@@ -1,1629 +0,0 @@
|
|
|
1
|
-
const path = require("path");
|
|
2
|
-
const fsp = require("fs/promises");
|
|
3
|
-
const fg = require("fast-glob");
|
|
4
|
-
const db = require("../db");
|
|
5
|
-
const {
|
|
6
|
-
workspaceRoot,
|
|
7
|
-
resolveWorkspacePath,
|
|
8
|
-
writeFile: writeWorkspaceFile,
|
|
9
|
-
} = require("../workspace");
|
|
10
|
-
const { runProcess } = require("../tools/process");
|
|
11
|
-
const { parseFile } = require("./parser");
|
|
12
|
-
const { analyzeFile } = require("./navigation");
|
|
13
|
-
const logger = require("../logger");
|
|
14
|
-
const { getTestSummary } = require("../tests");
|
|
15
|
-
|
|
16
|
-
const DEFAULT_GLOBS = ["**/*"];
|
|
17
|
-
const DEFAULT_IGNORE = ["node_modules/**", ".git/**", "data/**", "tmp/**"];
|
|
18
|
-
const MAX_FALLBACK_FILE_SIZE = 1024 * 1024; // 1MB
|
|
19
|
-
const MAX_RESULTS = 200;
|
|
20
|
-
const MAX_SYMBOL_FILE_SIZE = 512 * 1024; // 512KB
|
|
21
|
-
const DEFAULT_GRAPH_LIMIT = 250;
|
|
22
|
-
const CLAUDE_DOC_HEADER = "<!-- Generated by claude-code indexer -->";
|
|
23
|
-
const COMMON_DEP_EXTENSIONS = [
|
|
24
|
-
"js",
|
|
25
|
-
"jsx",
|
|
26
|
-
"ts",
|
|
27
|
-
"tsx",
|
|
28
|
-
"mjs",
|
|
29
|
-
"cjs",
|
|
30
|
-
"json",
|
|
31
|
-
"py",
|
|
32
|
-
"rb",
|
|
33
|
-
"go",
|
|
34
|
-
"rs",
|
|
35
|
-
"java",
|
|
36
|
-
"cs",
|
|
37
|
-
"cpp",
|
|
38
|
-
"c",
|
|
39
|
-
"h",
|
|
40
|
-
"hpp",
|
|
41
|
-
"swift",
|
|
42
|
-
"kt",
|
|
43
|
-
"kts",
|
|
44
|
-
"scala",
|
|
45
|
-
"sql",
|
|
46
|
-
"md",
|
|
47
|
-
];
|
|
48
|
-
const STYLE_GUIDE_RULES = [
|
|
49
|
-
{
|
|
50
|
-
match: (info) => path.basename(info.path) === ".editorconfig",
|
|
51
|
-
tool: "editorconfig",
|
|
52
|
-
detail: "EditorConfig configuration",
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
match: (info) =>
|
|
56
|
-
/^\.eslintrc(\..*)?$/.test(path.basename(info.path)) ||
|
|
57
|
-
["eslint.config.js", "eslint.config.cjs", "eslint.config.mjs"].includes(info.path),
|
|
58
|
-
tool: "eslint",
|
|
59
|
-
detail: "ESLint configuration",
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
match: (info) =>
|
|
63
|
-
/^\.?prettierrc(\..*)?$/.test(path.basename(info.path)) ||
|
|
64
|
-
["prettier.config.js", "prettier.config.cjs", "prettier.config.mjs"].includes(info.path),
|
|
65
|
-
tool: "prettier",
|
|
66
|
-
detail: "Prettier configuration",
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
match: (info) =>
|
|
70
|
-
/^\.stylelintrc(\..*)?$/.test(path.basename(info.path)) ||
|
|
71
|
-
info.path === "stylelint.config.js",
|
|
72
|
-
tool: "stylelint",
|
|
73
|
-
detail: "Stylelint configuration",
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
match: (info) => info.path === ".clang-format" || info.path === ".clang-tidy",
|
|
77
|
-
tool: "clang_format",
|
|
78
|
-
detail: "Clang formatting configuration",
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
match: (info) =>
|
|
82
|
-
info.path === ".flake8" ||
|
|
83
|
-
info.path === ".pylintrc" ||
|
|
84
|
-
info.path === ".pep8",
|
|
85
|
-
tool: "python_lint",
|
|
86
|
-
detail: "Python lint configuration",
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
match: (info) => info.path === "pyproject.toml",
|
|
90
|
-
tool: "pyproject",
|
|
91
|
-
detail: "Python pyproject configuration",
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
match: (info) => info.path === "setup.cfg",
|
|
95
|
-
tool: "python_setup_cfg",
|
|
96
|
-
detail: "Python setup.cfg configuration",
|
|
97
|
-
},
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
function inferStyleGuides(fileInfos) {
|
|
101
|
-
const matches = [];
|
|
102
|
-
const seen = new Set();
|
|
103
|
-
|
|
104
|
-
for (const info of fileInfos) {
|
|
105
|
-
for (const rule of STYLE_GUIDE_RULES) {
|
|
106
|
-
let matched = false;
|
|
107
|
-
try {
|
|
108
|
-
matched = rule.match(info);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
logger.debug({ err, file: info.path }, "Failed evaluating style guide rule");
|
|
111
|
-
}
|
|
112
|
-
if (matched) {
|
|
113
|
-
const key = `${rule.tool}:${info.path}`;
|
|
114
|
-
if (!seen.has(key)) {
|
|
115
|
-
seen.add(key);
|
|
116
|
-
matches.push({
|
|
117
|
-
tool: rule.tool,
|
|
118
|
-
path: info.path,
|
|
119
|
-
detail: rule.detail,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
matches.sort((a, b) => a.tool.localeCompare(b.tool) || a.path.localeCompare(b.path));
|
|
127
|
-
|
|
128
|
-
if (matches.length === 0) {
|
|
129
|
-
matches.push({
|
|
130
|
-
tool: "general",
|
|
131
|
-
path: null,
|
|
132
|
-
detail: "No explicit style guides detected. Consider adding lint/format tooling for key languages.",
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return matches;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function synthesiseStyleGuideInsights(styleGuides, languages) {
|
|
140
|
-
if (!Array.isArray(styleGuides) || styleGuides.length === 0) {
|
|
141
|
-
return ["No style configuration detected."];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const insights = [];
|
|
145
|
-
const languagesMentioned = new Set(languages ?? []);
|
|
146
|
-
|
|
147
|
-
for (const guide of styleGuides) {
|
|
148
|
-
if (guide.path) {
|
|
149
|
-
insights.push(`${guide.tool}: ${guide.path} (${guide.detail})`);
|
|
150
|
-
} else {
|
|
151
|
-
insights.push(`${guide.tool}: ${guide.detail}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (languagesMentioned.has("python") && !styleGuides.some((guide) => guide.tool.startsWith("python"))) {
|
|
156
|
-
insights.push("Python present but no lint/format configuration detected.");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return insights;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function normalisePatterns(patterns) {
|
|
163
|
-
if (!patterns) return DEFAULT_GLOBS;
|
|
164
|
-
if (typeof patterns === "string") return [patterns];
|
|
165
|
-
if (Array.isArray(patterns) && patterns.length > 0) return patterns.map(String);
|
|
166
|
-
return DEFAULT_GLOBS;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function normaliseIgnore(ignore) {
|
|
170
|
-
if (!ignore) return DEFAULT_IGNORE;
|
|
171
|
-
if (typeof ignore === "string") return [...DEFAULT_IGNORE, ignore];
|
|
172
|
-
if (Array.isArray(ignore)) return [...DEFAULT_IGNORE, ...ignore.map(String)];
|
|
173
|
-
return DEFAULT_IGNORE;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function listWorkspaceFiles(options = {}) {
|
|
177
|
-
const patterns = normalisePatterns(options.patterns);
|
|
178
|
-
const ignore = normaliseIgnore(options.ignore);
|
|
179
|
-
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 1000;
|
|
180
|
-
const includeDirectories = options.includeDirectories === true;
|
|
181
|
-
|
|
182
|
-
const entries = await fg(patterns, {
|
|
183
|
-
cwd: workspaceRoot,
|
|
184
|
-
ignore,
|
|
185
|
-
dot: false,
|
|
186
|
-
onlyFiles: !includeDirectories,
|
|
187
|
-
markDirectories: includeDirectories,
|
|
188
|
-
unique: true,
|
|
189
|
-
followSymbolicLinks: false,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const sliced = entries.slice(0, limit);
|
|
193
|
-
|
|
194
|
-
if (!options.withStats) {
|
|
195
|
-
return sliced.map((entry) => ({
|
|
196
|
-
path: entry,
|
|
197
|
-
type: entry.endsWith("/") ? "directory" : "file",
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const results = [];
|
|
202
|
-
for (const entry of sliced) {
|
|
203
|
-
try {
|
|
204
|
-
const absolute = resolveWorkspacePath(entry);
|
|
205
|
-
const stats = await fsp.stat(absolute);
|
|
206
|
-
results.push({
|
|
207
|
-
path: entry,
|
|
208
|
-
type: stats.isDirectory() ? "directory" : "file",
|
|
209
|
-
size: stats.size,
|
|
210
|
-
mtimeMs: stats.mtimeMs,
|
|
211
|
-
});
|
|
212
|
-
} catch (err) {
|
|
213
|
-
logger.warn({ err, entry }, "Failed to stat workspace entry");
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return results;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function parseRipgrepJson(stdout, limit) {
|
|
220
|
-
const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
|
|
221
|
-
const results = [];
|
|
222
|
-
for (const line of lines) {
|
|
223
|
-
if (results.length >= limit) break;
|
|
224
|
-
let parsed;
|
|
225
|
-
try {
|
|
226
|
-
parsed = JSON.parse(line);
|
|
227
|
-
} catch {
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (parsed.type !== "match") continue;
|
|
231
|
-
const data = parsed.data;
|
|
232
|
-
const pathText = data?.path?.text;
|
|
233
|
-
const linesText = data?.lines?.text;
|
|
234
|
-
const submatches = data?.submatches ?? [];
|
|
235
|
-
if (!pathText || typeof linesText !== "string") continue;
|
|
236
|
-
const relativePath = path.relative(workspaceRoot, path.resolve(workspaceRoot, pathText));
|
|
237
|
-
results.push({
|
|
238
|
-
path: relativePath,
|
|
239
|
-
line: data.line_number,
|
|
240
|
-
column: submatches[0]?.start ?? null,
|
|
241
|
-
match: linesText.trimEnd(),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return results;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function searchWithRipgrep({ query, regex, limit, ignore }) {
|
|
248
|
-
const args = [
|
|
249
|
-
"--json",
|
|
250
|
-
"--no-heading",
|
|
251
|
-
"--hidden",
|
|
252
|
-
"--line-number",
|
|
253
|
-
"--column",
|
|
254
|
-
"--color=never",
|
|
255
|
-
`--max-count=${limit}`,
|
|
256
|
-
];
|
|
257
|
-
ignore.forEach((glob) => {
|
|
258
|
-
args.push(`--glob=!${glob}`);
|
|
259
|
-
});
|
|
260
|
-
if (!regex) {
|
|
261
|
-
args.push("--fixed-strings");
|
|
262
|
-
}
|
|
263
|
-
args.push(query);
|
|
264
|
-
args.push(".");
|
|
265
|
-
|
|
266
|
-
const result = await runProcess({
|
|
267
|
-
command: "rg",
|
|
268
|
-
args,
|
|
269
|
-
cwd: workspaceRoot,
|
|
270
|
-
env: {},
|
|
271
|
-
timeoutMs: 10000,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
275
|
-
const error = new Error("ripgrep returned an error.");
|
|
276
|
-
error.stdout = result.stdout;
|
|
277
|
-
error.stderr = result.stderr;
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return parseRipgrepJson(result.stdout, limit);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function readFileExcerpt(relativePath, limitBytes = MAX_FALLBACK_FILE_SIZE) {
|
|
285
|
-
try {
|
|
286
|
-
const absolute = resolveWorkspacePath(relativePath);
|
|
287
|
-
const stats = await fsp.stat(absolute);
|
|
288
|
-
if (!stats.isFile() || stats.size > limitBytes) {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
const content = await fsp.readFile(absolute, "utf8");
|
|
292
|
-
return content;
|
|
293
|
-
} catch (err) {
|
|
294
|
-
logger.warn({ err, relativePath }, "Failed to read file during fallback search");
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const LANGUAGE_EXTENSIONS = {
|
|
300
|
-
js: "javascript",
|
|
301
|
-
mjs: "javascript",
|
|
302
|
-
cjs: "javascript",
|
|
303
|
-
ts: "typescript",
|
|
304
|
-
tsx: "typescript-react",
|
|
305
|
-
jsx: "javascript-react",
|
|
306
|
-
py: "python",
|
|
307
|
-
rb: "ruby",
|
|
308
|
-
java: "java",
|
|
309
|
-
go: "go",
|
|
310
|
-
rs: "rust",
|
|
311
|
-
php: "php",
|
|
312
|
-
cs: "csharp",
|
|
313
|
-
cpp: "cpp",
|
|
314
|
-
cxx: "cpp",
|
|
315
|
-
cc: "cpp",
|
|
316
|
-
h: "c-header",
|
|
317
|
-
hpp: "cpp-header",
|
|
318
|
-
json: "json",
|
|
319
|
-
yaml: "yaml",
|
|
320
|
-
yml: "yaml",
|
|
321
|
-
md: "markdown",
|
|
322
|
-
sh: "shell",
|
|
323
|
-
bash: "shell",
|
|
324
|
-
zsh: "shell",
|
|
325
|
-
fish: "shell",
|
|
326
|
-
swift: "swift",
|
|
327
|
-
kt: "kotlin",
|
|
328
|
-
kts: "kotlin",
|
|
329
|
-
scala: "scala",
|
|
330
|
-
sql: "sql",
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
function extractSymbols(relativePath, content, language) {
|
|
334
|
-
if (!language || typeof content !== "string") return [];
|
|
335
|
-
const lang = language.toLowerCase();
|
|
336
|
-
const lines = content.split(/\r?\n/);
|
|
337
|
-
const symbols = [];
|
|
338
|
-
|
|
339
|
-
const pushSymbol = (name, kind, lineIndex, column = 1, metadata) => {
|
|
340
|
-
if (!name) return;
|
|
341
|
-
symbols.push({
|
|
342
|
-
name,
|
|
343
|
-
kind,
|
|
344
|
-
line: lineIndex + 1,
|
|
345
|
-
column,
|
|
346
|
-
metadata: metadata ?? null,
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const simpleMatch = (regex, kind) => {
|
|
351
|
-
lines.forEach((line, idx) => {
|
|
352
|
-
const match = line.match(regex);
|
|
353
|
-
if (match && match[1]) {
|
|
354
|
-
const col = line.indexOf(match[1]) + 1 || 1;
|
|
355
|
-
pushSymbol(match[1], kind, idx, col);
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
switch (lang) {
|
|
361
|
-
case "javascript":
|
|
362
|
-
case "javascript-react":
|
|
363
|
-
case "typescript":
|
|
364
|
-
case "typescript-react":
|
|
365
|
-
simpleMatch(/\bfunction\s+([A-Za-z0-9_$]+)\s*\(/, "function");
|
|
366
|
-
simpleMatch(/\bclass\s+([A-Za-z0-9_$]+)/, "class");
|
|
367
|
-
simpleMatch(/\bconst\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?\(/, "function");
|
|
368
|
-
simpleMatch(/\bexport\s+default\s+function\s+([A-Za-z0-9_$]+)\s*\(/, "function");
|
|
369
|
-
break;
|
|
370
|
-
case "python":
|
|
371
|
-
simpleMatch(/^\s*def\s+([A-Za-z0-9_]+)\s*\(/, "function");
|
|
372
|
-
simpleMatch(/^\s*class\s+([A-Za-z0-9_]+)/, "class");
|
|
373
|
-
break;
|
|
374
|
-
case "go":
|
|
375
|
-
lines.forEach((line, idx) => {
|
|
376
|
-
const match = line.match(/^\s*func\s+(?:\([^)]+\)\s*)?([A-Za-z0-9_]+)\s*\(/);
|
|
377
|
-
if (match && match[1]) {
|
|
378
|
-
pushSymbol(match[1], "function", idx, line.indexOf(match[1]) + 1);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
break;
|
|
382
|
-
case "java":
|
|
383
|
-
case "csharp":
|
|
384
|
-
simpleMatch(/\bclass\s+([A-Za-z0-9_]+)/, "class");
|
|
385
|
-
simpleMatch(/\binterface\s+([A-Za-z0-9_]+)/, "interface");
|
|
386
|
-
simpleMatch(/\benum\s+([A-Za-z0-9_]+)/, "enum");
|
|
387
|
-
break;
|
|
388
|
-
case "rust":
|
|
389
|
-
simpleMatch(/\bfn\s+([A-Za-z0-9_]+)\s*\(/, "function");
|
|
390
|
-
simpleMatch(/\bstruct\s+([A-Za-z0-9_]+)/, "struct");
|
|
391
|
-
simpleMatch(/\benum\s+([A-Za-z0-9_]+)/, "enum");
|
|
392
|
-
break;
|
|
393
|
-
case "php":
|
|
394
|
-
simpleMatch(/\bfunction\s+([A-Za-z0-9_]+)\s*\(/, "function");
|
|
395
|
-
simpleMatch(/\bclass\s+([A-Za-z0-9_]+)/, "class");
|
|
396
|
-
break;
|
|
397
|
-
case "ruby":
|
|
398
|
-
simpleMatch(/^\s*def\s+([A-Za-z0-9_!?]+)/, "method");
|
|
399
|
-
simpleMatch(/^\s*class\s+([A-Za-z0-9_:]+)/, "class");
|
|
400
|
-
simpleMatch(/^\s*module\s+([A-Za-z0-9_:]+)/, "module");
|
|
401
|
-
break;
|
|
402
|
-
case "markdown":
|
|
403
|
-
lines.forEach((line, idx) => {
|
|
404
|
-
const match = line.match(/^(#+)\s+(.*)$/);
|
|
405
|
-
if (match) {
|
|
406
|
-
const title = match[2].trim();
|
|
407
|
-
pushSymbol(title, `heading_${match[1].length}`, idx, line.indexOf(title) + 1);
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
break;
|
|
411
|
-
default:
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
return symbols;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const clearFilesStmt = db.prepare("DELETE FROM files");
|
|
418
|
-
const clearSymbolsStmt = db.prepare("DELETE FROM symbols");
|
|
419
|
-
const clearSymbolRefsStmt = db.prepare("DELETE FROM symbol_references");
|
|
420
|
-
const clearFrameworkStmt = db.prepare("DELETE FROM framework_signals");
|
|
421
|
-
const clearDependenciesStmt = db.prepare("DELETE FROM file_dependencies");
|
|
422
|
-
const upsertMetadataStmt = db.prepare(
|
|
423
|
-
`INSERT INTO workspace_metadata (key, value)
|
|
424
|
-
VALUES (@key, @value)
|
|
425
|
-
ON CONFLICT(key) DO UPDATE SET value=excluded.value`,
|
|
426
|
-
);
|
|
427
|
-
const selectMetadataStmt = db.prepare("SELECT value FROM workspace_metadata WHERE key = ?");
|
|
428
|
-
const insertFileStmt = db.prepare(
|
|
429
|
-
`INSERT INTO files (path, size_bytes, mtime_ms, language, summary)
|
|
430
|
-
VALUES (@path, @size_bytes, @mtime_ms, @language, @summary)
|
|
431
|
-
ON CONFLICT(path) DO UPDATE SET
|
|
432
|
-
size_bytes=excluded.size_bytes,
|
|
433
|
-
mtime_ms=excluded.mtime_ms,
|
|
434
|
-
language=excluded.language,
|
|
435
|
-
summary=excluded.summary`,
|
|
436
|
-
);
|
|
437
|
-
const insertFrameworkStmt = db.prepare(
|
|
438
|
-
`INSERT INTO framework_signals (type, file_path, detail, metadata)
|
|
439
|
-
VALUES (@type, @file_path, @detail, @metadata)`,
|
|
440
|
-
);
|
|
441
|
-
const insertDependencyStmt = db.prepare(
|
|
442
|
-
`INSERT INTO file_dependencies (from_path, to_path, kind, metadata)
|
|
443
|
-
VALUES (@from_path, @to_path, @kind, @metadata)`,
|
|
444
|
-
);
|
|
445
|
-
const insertSymbolStmt = db.prepare(
|
|
446
|
-
`INSERT INTO symbols (file_path, name, kind, line, column, metadata)
|
|
447
|
-
VALUES (@file_path, @name, @kind, @line, @column, @metadata)`,
|
|
448
|
-
);
|
|
449
|
-
const insertSymbolReferenceStmt = db.prepare(
|
|
450
|
-
`INSERT INTO symbol_references (symbol_id, file_path, line, column, snippet, metadata)
|
|
451
|
-
VALUES (@symbol_id, @file_path, @line, @column, @snippet, @metadata)`,
|
|
452
|
-
);
|
|
453
|
-
const selectDefinitionByLocationStmt = db.prepare(
|
|
454
|
-
`SELECT s.name,
|
|
455
|
-
s.kind,
|
|
456
|
-
s.file_path AS definition_path,
|
|
457
|
-
s.line AS definition_line,
|
|
458
|
-
s.column AS definition_column,
|
|
459
|
-
r.file_path AS reference_path,
|
|
460
|
-
r.line AS reference_line,
|
|
461
|
-
r.column AS reference_column,
|
|
462
|
-
r.snippet,
|
|
463
|
-
ABS(COALESCE(r.column, 0) - COALESCE(@column, 0)) AS column_distance
|
|
464
|
-
FROM symbol_references r
|
|
465
|
-
JOIN symbols s ON r.symbol_id = s.id
|
|
466
|
-
WHERE r.file_path = @filePath
|
|
467
|
-
AND r.line = @line
|
|
468
|
-
AND (@column IS NULL OR r.column = @column)
|
|
469
|
-
ORDER BY column_distance ASC,
|
|
470
|
-
s.name ASC
|
|
471
|
-
LIMIT @limit`,
|
|
472
|
-
);
|
|
473
|
-
const selectDefinitionsBySymbolStmt = db.prepare(
|
|
474
|
-
`SELECT name,
|
|
475
|
-
kind,
|
|
476
|
-
file_path,
|
|
477
|
-
line,
|
|
478
|
-
column,
|
|
479
|
-
metadata
|
|
480
|
-
FROM symbols
|
|
481
|
-
WHERE name = @name
|
|
482
|
-
ORDER BY line ASC, file_path ASC
|
|
483
|
-
LIMIT @limit`,
|
|
484
|
-
);
|
|
485
|
-
|
|
486
|
-
function inferLanguage(relativePath) {
|
|
487
|
-
const ext = path.extname(relativePath).replace(".", "").toLowerCase();
|
|
488
|
-
return LANGUAGE_EXTENSIONS[ext] ?? null;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function summariseDependencies(packageJson) {
|
|
492
|
-
if (!packageJson) return null;
|
|
493
|
-
const dependencies = Object.keys(packageJson.dependencies ?? {});
|
|
494
|
-
const devDependencies = Object.keys(packageJson.devDependencies ?? {});
|
|
495
|
-
const picks = [...dependencies.slice(0, 5), ...devDependencies.slice(0, 3)];
|
|
496
|
-
if (picks.length === 0) return null;
|
|
497
|
-
return {
|
|
498
|
-
sampleDependencies: picks,
|
|
499
|
-
totalDependencies: dependencies.length,
|
|
500
|
-
totalDevDependencies: devDependencies.length,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function escapeRegex(value) {
|
|
505
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Regex pattern cache for performance
|
|
509
|
-
const regexCache = new Map();
|
|
510
|
-
const MAX_REGEX_CACHE_SIZE = 10000;
|
|
511
|
-
|
|
512
|
-
function getCachedRegex(symbolName) {
|
|
513
|
-
if (!regexCache.has(symbolName)) {
|
|
514
|
-
const escaped = escapeRegex(symbolName);
|
|
515
|
-
const regex = new RegExp(`\\b${escaped}\\b`, "g");
|
|
516
|
-
regexCache.set(symbolName, regex);
|
|
517
|
-
|
|
518
|
-
// Prevent unbounded growth
|
|
519
|
-
if (regexCache.size > MAX_REGEX_CACHE_SIZE) {
|
|
520
|
-
const firstKey = regexCache.keys().next().value;
|
|
521
|
-
regexCache.delete(firstKey);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
return regexCache.get(symbolName);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function safeParseJson(value, fallback = null) {
|
|
528
|
-
if (value === null || value === undefined) return fallback;
|
|
529
|
-
if (typeof value !== "string") return fallback;
|
|
530
|
-
try {
|
|
531
|
-
return JSON.parse(value);
|
|
532
|
-
} catch (err) {
|
|
533
|
-
logger.debug({ err }, "Failed to parse JSON metadata");
|
|
534
|
-
return fallback;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function detectFrameworks(fileInfos) {
|
|
539
|
-
const frameworks = new Set();
|
|
540
|
-
const signals = [];
|
|
541
|
-
|
|
542
|
-
const addSignal = (type, filePath, detail, metadata) => {
|
|
543
|
-
frameworks.add(type);
|
|
544
|
-
signals.push({
|
|
545
|
-
type,
|
|
546
|
-
file_path: filePath,
|
|
547
|
-
detail,
|
|
548
|
-
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
549
|
-
});
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
const fileMap = new Map(fileInfos.map((info) => [info.path, info]));
|
|
553
|
-
|
|
554
|
-
if (fileMap.has("package.json")) {
|
|
555
|
-
const info = fileMap.get("package.json");
|
|
556
|
-
const metadata = summariseDependencies(info.packageJson);
|
|
557
|
-
addSignal("node", info.path, "package.json detected", metadata);
|
|
558
|
-
if (metadata?.sampleDependencies?.some((dep) => dep.includes("react"))) {
|
|
559
|
-
addSignal("react", info.path, "React dependency detected", metadata);
|
|
560
|
-
}
|
|
561
|
-
if (metadata?.sampleDependencies?.some((dep) => dep.includes("next"))) {
|
|
562
|
-
addSignal("nextjs", info.path, "Next.js dependency detected", metadata);
|
|
563
|
-
}
|
|
564
|
-
if (metadata?.sampleDependencies?.some((dep) => dep.includes("express"))) {
|
|
565
|
-
addSignal("express", info.path, "Express dependency detected", metadata);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (fileMap.has("requirements.txt")) {
|
|
570
|
-
addSignal("python", "requirements.txt", "requirements.txt detected");
|
|
571
|
-
}
|
|
572
|
-
if (fileMap.has("pyproject.toml")) {
|
|
573
|
-
addSignal("python", "pyproject.toml", "pyproject.toml detected");
|
|
574
|
-
}
|
|
575
|
-
if (fileMap.has("Pipfile")) {
|
|
576
|
-
addSignal("python", "Pipfile", "Pipfile detected");
|
|
577
|
-
}
|
|
578
|
-
if (fileMap.has("Gemfile")) {
|
|
579
|
-
addSignal("ruby", "Gemfile", "Gemfile detected");
|
|
580
|
-
}
|
|
581
|
-
if (fileMap.has("pom.xml")) {
|
|
582
|
-
addSignal("java", "pom.xml", "Maven project detected");
|
|
583
|
-
}
|
|
584
|
-
if (fileMap.has("build.gradle") || fileMap.has("build.gradle.kts")) {
|
|
585
|
-
addSignal("gradle", "build.gradle(.kts)", "Gradle build file detected");
|
|
586
|
-
}
|
|
587
|
-
if (fileMap.has("go.mod")) {
|
|
588
|
-
addSignal("go", "go.mod", "Go module detected");
|
|
589
|
-
}
|
|
590
|
-
if (fileMap.has("Cargo.toml")) {
|
|
591
|
-
addSignal("rust", "Cargo.toml", "Cargo crate detected");
|
|
592
|
-
}
|
|
593
|
-
if (fileMap.has("composer.json")) {
|
|
594
|
-
addSignal("php", "composer.json", "Composer project detected");
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return {
|
|
598
|
-
frameworks: Array.from(frameworks),
|
|
599
|
-
signals,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function computeTopDependencies(fileInfos, dependenciesRaw) {
|
|
604
|
-
const counts = new Map();
|
|
605
|
-
dependenciesRaw.forEach((dep) => {
|
|
606
|
-
if (!dep.to_path || dep.to_path.startsWith("..")) return;
|
|
607
|
-
const key = dep.to_path;
|
|
608
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
609
|
-
});
|
|
610
|
-
return Array.from(counts.entries())
|
|
611
|
-
.map(([path, count]) => ({ path, count }))
|
|
612
|
-
.sort((a, b) => b.count - a.count)
|
|
613
|
-
.slice(0, 10);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function computeDependencyGraph({ dependenciesRaw, limit = DEFAULT_GRAPH_LIMIT }) {
|
|
617
|
-
const dependencies = Array.isArray(dependenciesRaw) ? dependenciesRaw : [];
|
|
618
|
-
const nodes = new Map();
|
|
619
|
-
const edges = [];
|
|
620
|
-
let totalEdges = 0;
|
|
621
|
-
|
|
622
|
-
for (const dep of dependencies) {
|
|
623
|
-
if (!dep.from_path || !dep.to_path) continue;
|
|
624
|
-
|
|
625
|
-
const fromExt = path.extname(dep.from_path).replace(".", "").toLowerCase();
|
|
626
|
-
const toExt = path.extname(dep.to_path).replace(".", "").toLowerCase();
|
|
627
|
-
|
|
628
|
-
if (!COMMON_DEP_EXTENSIONS.includes(fromExt) || !COMMON_DEP_EXTENSIONS.includes(toExt)) {
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (!nodes.has(dep.from_path)) {
|
|
633
|
-
nodes.set(dep.from_path, {
|
|
634
|
-
id: dep.from_path,
|
|
635
|
-
language: path.extname(dep.from_path).replace(".", "") || null,
|
|
636
|
-
edgesOut: 0,
|
|
637
|
-
edgesIn: 0,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
if (!nodes.has(dep.to_path)) {
|
|
641
|
-
nodes.set(dep.to_path, {
|
|
642
|
-
id: dep.to_path,
|
|
643
|
-
language: path.extname(dep.to_path).replace(".", "") || null,
|
|
644
|
-
edgesOut: 0,
|
|
645
|
-
edgesIn: 0,
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
nodes.get(dep.from_path).edgesOut += 1;
|
|
650
|
-
nodes.get(dep.to_path).edgesIn += 1;
|
|
651
|
-
|
|
652
|
-
let metadata = null;
|
|
653
|
-
if (dep.metadata !== null && dep.metadata !== undefined) {
|
|
654
|
-
if (typeof dep.metadata === "string") {
|
|
655
|
-
try {
|
|
656
|
-
metadata = JSON.parse(dep.metadata);
|
|
657
|
-
} catch (err) {
|
|
658
|
-
metadata = { raw: dep.metadata };
|
|
659
|
-
}
|
|
660
|
-
} else {
|
|
661
|
-
metadata = dep.metadata;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
edges.push({
|
|
666
|
-
from: dep.from_path,
|
|
667
|
-
to: dep.to_path,
|
|
668
|
-
kind: dep.kind ?? "reference",
|
|
669
|
-
metadata,
|
|
670
|
-
});
|
|
671
|
-
totalEdges += 1;
|
|
672
|
-
|
|
673
|
-
if (edges.length >= limit) break;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const topNodes = Array.from(nodes.values())
|
|
677
|
-
.sort((a, b) => b.edgesOut + b.edgesIn - (a.edgesOut + a.edgesIn))
|
|
678
|
-
.slice(0, limit);
|
|
679
|
-
|
|
680
|
-
const nodeIds = new Set(topNodes.map((node) => node.id));
|
|
681
|
-
const filteredEdges = edges.filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to));
|
|
682
|
-
|
|
683
|
-
return {
|
|
684
|
-
nodes: topNodes,
|
|
685
|
-
edges: filteredEdges,
|
|
686
|
-
totalNodes: nodes.size,
|
|
687
|
-
totalEdges,
|
|
688
|
-
limitApplied: edges.length >= limit,
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function summariseFrameworkSignals(frameworkSignals = []) {
|
|
693
|
-
const groups = frameworkSignals.reduce((acc, signal) => {
|
|
694
|
-
const key = signal.type ?? "other";
|
|
695
|
-
const list = acc.get(key) ?? [];
|
|
696
|
-
list.push(signal);
|
|
697
|
-
acc.set(key, list);
|
|
698
|
-
return acc;
|
|
699
|
-
}, new Map());
|
|
700
|
-
|
|
701
|
-
return Array.from(groups.entries()).map(([framework, signals]) => ({
|
|
702
|
-
framework,
|
|
703
|
-
count: signals.length,
|
|
704
|
-
samples: signals.slice(0, 5),
|
|
705
|
-
}));
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function buildClaudeDocContent(summary) {
|
|
709
|
-
const lines = [];
|
|
710
|
-
lines.push(CLAUDE_DOC_HEADER);
|
|
711
|
-
lines.push("");
|
|
712
|
-
lines.push("# Project Overview");
|
|
713
|
-
lines.push("");
|
|
714
|
-
lines.push(`- Workspace root: \`${summary.workspaceRoot}\``);
|
|
715
|
-
lines.push(`- Indexed at: ${summary.indexedAt}`);
|
|
716
|
-
lines.push(`- Files indexed: ${summary.fileCount}`);
|
|
717
|
-
lines.push("");
|
|
718
|
-
|
|
719
|
-
if (Array.isArray(summary.languageStats) && summary.languageStats.length) {
|
|
720
|
-
lines.push("## Language Mix");
|
|
721
|
-
lines.push("");
|
|
722
|
-
summary.languageStats.slice(0, 10).forEach((lang) => {
|
|
723
|
-
lines.push(`- ${lang.language}: ${lang.files} files (${lang.percentage}%)`);
|
|
724
|
-
});
|
|
725
|
-
lines.push("");
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (Array.isArray(summary.frameworks) && summary.frameworks.length) {
|
|
729
|
-
lines.push("## Framework Signals");
|
|
730
|
-
lines.push("");
|
|
731
|
-
const grouped = summariseFrameworkSignals(summary.frameworkSignals);
|
|
732
|
-
grouped.forEach((item) => {
|
|
733
|
-
lines.push(`- **${item.framework}** (${item.count} signals)`);
|
|
734
|
-
item.samples.forEach((signal) => {
|
|
735
|
-
lines.push(` - ${signal.detail} (${signal.file_path})`);
|
|
736
|
-
});
|
|
737
|
-
});
|
|
738
|
-
lines.push("");
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (Array.isArray(summary.styleGuideInsights) && summary.styleGuideInsights.length) {
|
|
742
|
-
lines.push("## Style Guide Insights");
|
|
743
|
-
lines.push("");
|
|
744
|
-
summary.styleGuideInsights.forEach((insight) => {
|
|
745
|
-
lines.push(`- ${insight}`);
|
|
746
|
-
});
|
|
747
|
-
lines.push("");
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (Array.isArray(summary.topDependencies) && summary.topDependencies.length) {
|
|
751
|
-
lines.push("## Top Workspace Dependencies");
|
|
752
|
-
lines.push("");
|
|
753
|
-
summary.topDependencies.slice(0, 10).forEach((dep) => {
|
|
754
|
-
lines.push(`- ${dep.path} (refs: ${dep.count})`);
|
|
755
|
-
});
|
|
756
|
-
lines.push("");
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
if (summary.dependencyGraph?.edges?.length) {
|
|
760
|
-
lines.push("## Dependency Graph Snapshot");
|
|
761
|
-
lines.push("");
|
|
762
|
-
lines.push(
|
|
763
|
-
`Graph nodes: ${summary.dependencyGraph.nodes.length} (of ${summary.dependencyGraph.totalNodes}), edges: ${summary.dependencyGraph.edges.length} (of ${summary.dependencyGraph.totalEdges})`,
|
|
764
|
-
);
|
|
765
|
-
lines.push("");
|
|
766
|
-
const sampleEdges = summary.dependencyGraph.edges.slice(0, 15);
|
|
767
|
-
sampleEdges.forEach((edge) => {
|
|
768
|
-
lines.push(`- \`${edge.from}\` → \`${edge.to}\` (${edge.kind})`);
|
|
769
|
-
});
|
|
770
|
-
lines.push("");
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
lines.push("## Re-index Guidance");
|
|
774
|
-
lines.push("");
|
|
775
|
-
lines.push(
|
|
776
|
-
"Run `workspace_index_rebuild` to refresh this document after making large changes.",
|
|
777
|
-
);
|
|
778
|
-
lines.push("");
|
|
779
|
-
|
|
780
|
-
return `${lines.join("\n").trim()}\n`;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
async function ensureClaudeDoc(summary) {
|
|
784
|
-
const content = buildClaudeDocContent(summary);
|
|
785
|
-
let existing = null;
|
|
786
|
-
try {
|
|
787
|
-
const absolute = resolveWorkspacePath("CLAUDE.md");
|
|
788
|
-
existing = await fsp.readFile(absolute, "utf8");
|
|
789
|
-
} catch (err) {
|
|
790
|
-
if (err.code !== "ENOENT") {
|
|
791
|
-
throw err;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (existing && !existing.startsWith(CLAUDE_DOC_HEADER)) {
|
|
796
|
-
logger.debug("Skipping CLAUDE.md overwrite; existing file is user-authored.");
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
if (existing && existing.trim() === content.trim()) {
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
await writeWorkspaceFile("CLAUDE.md", content, { encoding: "utf8", createParents: true });
|
|
804
|
-
logger.info("Updated CLAUDE.md project overview");
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function buildProjectSummary({ fileInfos, frameworks, dependenciesRaw }) {
|
|
808
|
-
const languageCounts = fileInfos.reduce((acc, item) => {
|
|
809
|
-
if (item.language) {
|
|
810
|
-
acc[item.language] = (acc[item.language] ?? 0) + 1;
|
|
811
|
-
}
|
|
812
|
-
return acc;
|
|
813
|
-
}, {});
|
|
814
|
-
const languageStats = Object.entries(languageCounts)
|
|
815
|
-
.map(([language, count]) => ({
|
|
816
|
-
language,
|
|
817
|
-
files: count,
|
|
818
|
-
percentage: Number(((count / Math.max(fileInfos.length, 1)) * 100).toFixed(2)),
|
|
819
|
-
}))
|
|
820
|
-
.sort((a, b) => b.files - a.files);
|
|
821
|
-
|
|
822
|
-
const styleGuides = inferStyleGuides(fileInfos);
|
|
823
|
-
const styleGuideInsights = synthesiseStyleGuideInsights(
|
|
824
|
-
styleGuides,
|
|
825
|
-
languageStats.map((item) => item.language),
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
const summary = {
|
|
829
|
-
indexedAt: new Date().toISOString(),
|
|
830
|
-
workspaceRoot,
|
|
831
|
-
fileCount: fileInfos.length,
|
|
832
|
-
languages: languageStats.map((item) => item.language),
|
|
833
|
-
languageStats,
|
|
834
|
-
frameworks: frameworks.frameworks.sort(),
|
|
835
|
-
frameworkSignals: frameworks.signals.map((signal) => ({
|
|
836
|
-
type: signal.type,
|
|
837
|
-
file_path: signal.file_path,
|
|
838
|
-
detail: signal.detail,
|
|
839
|
-
})),
|
|
840
|
-
topDependencies: computeTopDependencies(fileInfos, dependenciesRaw),
|
|
841
|
-
dependencyGraph: computeDependencyGraph({ dependenciesRaw }),
|
|
842
|
-
styleGuides,
|
|
843
|
-
styleGuideInsights,
|
|
844
|
-
tests: getTestSummary({ includeRecent: false }),
|
|
845
|
-
};
|
|
846
|
-
return summary;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function storeProjectSummary(summary) {
|
|
850
|
-
upsertMetadataStmt.run({
|
|
851
|
-
key: "project_summary",
|
|
852
|
-
value: JSON.stringify(summary),
|
|
853
|
-
});
|
|
854
|
-
upsertMetadataStmt.run({
|
|
855
|
-
key: "last_indexed_at",
|
|
856
|
-
value: String(Date.now()),
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function readProjectSummary() {
|
|
861
|
-
const row = selectMetadataStmt.get("project_summary");
|
|
862
|
-
if (!row) return null;
|
|
863
|
-
try {
|
|
864
|
-
return JSON.parse(row.value);
|
|
865
|
-
} catch (err) {
|
|
866
|
-
logger.warn({ err }, "Failed to parse project summary metadata");
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const FALLBACK_REFERENCE_SAMPLE_LIMIT = 2000;
|
|
872
|
-
|
|
873
|
-
function normaliseDefinition(definition, { engine, relativePath }) {
|
|
874
|
-
if (!definition || typeof definition.name !== "string") return null;
|
|
875
|
-
const name = definition.name.trim();
|
|
876
|
-
if (!name) return null;
|
|
877
|
-
const line =
|
|
878
|
-
Number.isFinite(definition.line) && definition.line > 0
|
|
879
|
-
? Math.trunc(definition.line)
|
|
880
|
-
: Number.isFinite(definition.start?.line) && definition.start.line > 0
|
|
881
|
-
? Math.trunc(definition.start.line)
|
|
882
|
-
: Number.isFinite(definition.loc?.start?.line) && definition.loc.start.line > 0
|
|
883
|
-
? Math.trunc(definition.loc.start.line)
|
|
884
|
-
: null;
|
|
885
|
-
if (!line) return null;
|
|
886
|
-
let column =
|
|
887
|
-
Number.isFinite(definition.column) && definition.column > 0
|
|
888
|
-
? Math.trunc(definition.column)
|
|
889
|
-
: Number.isFinite(definition.start?.column) && definition.start.column > 0
|
|
890
|
-
? Math.trunc(definition.start.column)
|
|
891
|
-
: Number.isFinite(definition.loc?.start?.column) && definition.loc.start.column > 0
|
|
892
|
-
? Math.trunc(definition.loc.start.column)
|
|
893
|
-
: 1;
|
|
894
|
-
if (column <= 0) column = 1;
|
|
895
|
-
const metadata =
|
|
896
|
-
typeof definition.metadata === "object" && definition.metadata !== null
|
|
897
|
-
? { ...definition.metadata }
|
|
898
|
-
: {};
|
|
899
|
-
if (engine) metadata.engine = engine;
|
|
900
|
-
if (relativePath && !metadata.filePath) {
|
|
901
|
-
metadata.filePath = relativePath;
|
|
902
|
-
}
|
|
903
|
-
return {
|
|
904
|
-
name,
|
|
905
|
-
kind: definition.kind ?? definition.type ?? null,
|
|
906
|
-
line,
|
|
907
|
-
column,
|
|
908
|
-
metadata: Object.keys(metadata).length ? metadata : null,
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function normaliseDependency(dep, { engine, fromPath }) {
|
|
913
|
-
if (!dep || typeof dep.path !== "string") return null;
|
|
914
|
-
const pathValue = dep.path.trim();
|
|
915
|
-
if (!pathValue) return null;
|
|
916
|
-
const kind = dep.kind ?? dep.type ?? "reference";
|
|
917
|
-
const metadata =
|
|
918
|
-
typeof dep.metadata === "object" && dep.metadata !== null ? { ...dep.metadata } : {};
|
|
919
|
-
if (dep.clause && !metadata.clause) {
|
|
920
|
-
metadata.clause = dep.clause;
|
|
921
|
-
}
|
|
922
|
-
if (dep.line && Number.isFinite(dep.line)) {
|
|
923
|
-
metadata.line = Math.trunc(dep.line);
|
|
924
|
-
}
|
|
925
|
-
if (dep.column && Number.isFinite(dep.column)) {
|
|
926
|
-
metadata.column = Math.trunc(dep.column);
|
|
927
|
-
}
|
|
928
|
-
if (engine) metadata.engine = engine;
|
|
929
|
-
if (fromPath && !metadata.fromPath) {
|
|
930
|
-
metadata.fromPath = fromPath;
|
|
931
|
-
}
|
|
932
|
-
return {
|
|
933
|
-
from_path: fromPath ?? null,
|
|
934
|
-
to_path: pathValue,
|
|
935
|
-
kind,
|
|
936
|
-
metadata: Object.keys(metadata).length ? metadata : null,
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
async function rebuildWorkspaceIndex(options = {}) {
|
|
941
|
-
const patterns = normalisePatterns(options.patterns);
|
|
942
|
-
const ignore = normaliseIgnore(options.ignore);
|
|
943
|
-
|
|
944
|
-
logger.info(
|
|
945
|
-
{
|
|
946
|
-
workspaceRoot,
|
|
947
|
-
patterns,
|
|
948
|
-
ignore,
|
|
949
|
-
},
|
|
950
|
-
"Rebuilding workspace index",
|
|
951
|
-
);
|
|
952
|
-
|
|
953
|
-
const entries = await fg(patterns, {
|
|
954
|
-
cwd: workspaceRoot,
|
|
955
|
-
ignore,
|
|
956
|
-
onlyFiles: true,
|
|
957
|
-
dot: true,
|
|
958
|
-
followSymbolicLinks: false,
|
|
959
|
-
unique: true,
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
const fileInfos = [];
|
|
963
|
-
const fileContents = new Map();
|
|
964
|
-
const navigationData = new Map();
|
|
965
|
-
let referenceCount = 0;
|
|
966
|
-
|
|
967
|
-
for (const relativePath of entries) {
|
|
968
|
-
try {
|
|
969
|
-
const absolute = resolveWorkspacePath(relativePath);
|
|
970
|
-
const stats = await fsp.stat(absolute);
|
|
971
|
-
if (!stats.isFile()) continue;
|
|
972
|
-
const language = inferLanguage(relativePath);
|
|
973
|
-
|
|
974
|
-
const info = {
|
|
975
|
-
path: relativePath,
|
|
976
|
-
size_bytes: stats.size,
|
|
977
|
-
mtime_ms: stats.mtimeMs,
|
|
978
|
-
language,
|
|
979
|
-
summary: null,
|
|
980
|
-
symbols: [],
|
|
981
|
-
dependencies: [],
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
if (relativePath === "package.json") {
|
|
985
|
-
try {
|
|
986
|
-
const pkgRaw = await fsp.readFile(absolute, "utf8");
|
|
987
|
-
info.packageJson = JSON.parse(pkgRaw);
|
|
988
|
-
} catch (err) {
|
|
989
|
-
logger.warn({ err }, "Failed to parse package.json for framework detection");
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (language && stats.size <= MAX_SYMBOL_FILE_SIZE) {
|
|
994
|
-
try {
|
|
995
|
-
const content = await fsp.readFile(absolute, "utf8");
|
|
996
|
-
fileContents.set(relativePath, content);
|
|
997
|
-
|
|
998
|
-
let navResult = null;
|
|
999
|
-
try {
|
|
1000
|
-
navResult = analyzeFile({
|
|
1001
|
-
relativePath,
|
|
1002
|
-
content,
|
|
1003
|
-
language,
|
|
1004
|
-
});
|
|
1005
|
-
} catch (analysisErr) {
|
|
1006
|
-
logger.debug(
|
|
1007
|
-
{ err: analysisErr, relativePath, language },
|
|
1008
|
-
"Structured navigation analysis failed",
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
if (navResult) {
|
|
1013
|
-
navigationData.set(relativePath, navResult);
|
|
1014
|
-
const definitions =
|
|
1015
|
-
(Array.isArray(navResult.definitions) && navResult.definitions.length
|
|
1016
|
-
? navResult.definitions
|
|
1017
|
-
: Array.isArray(navResult.symbols)
|
|
1018
|
-
? navResult.symbols
|
|
1019
|
-
: []
|
|
1020
|
-
).map((definition) =>
|
|
1021
|
-
normaliseDefinition(definition, {
|
|
1022
|
-
engine: navResult.engine,
|
|
1023
|
-
relativePath,
|
|
1024
|
-
}),
|
|
1025
|
-
).filter(Boolean);
|
|
1026
|
-
if (definitions.length) {
|
|
1027
|
-
info.symbols = definitions;
|
|
1028
|
-
}
|
|
1029
|
-
const deps =
|
|
1030
|
-
Array.isArray(navResult.dependencies) && navResult.dependencies.length
|
|
1031
|
-
? navResult.dependencies
|
|
1032
|
-
.map((dep) =>
|
|
1033
|
-
normaliseDependency(dep, {
|
|
1034
|
-
engine: navResult.engine,
|
|
1035
|
-
fromPath: relativePath,
|
|
1036
|
-
}),
|
|
1037
|
-
)
|
|
1038
|
-
.filter(Boolean)
|
|
1039
|
-
: [];
|
|
1040
|
-
if (deps.length) {
|
|
1041
|
-
info.dependencies = deps;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
if (!info.symbols.length) {
|
|
1046
|
-
const parsed = parseFile(relativePath, content, language);
|
|
1047
|
-
if (parsed) {
|
|
1048
|
-
const definitions =
|
|
1049
|
-
(Array.isArray(parsed.definitions) && parsed.definitions.length
|
|
1050
|
-
? parsed.definitions
|
|
1051
|
-
: Array.isArray(parsed.symbols)
|
|
1052
|
-
? parsed.symbols
|
|
1053
|
-
: []
|
|
1054
|
-
).map((definition) =>
|
|
1055
|
-
normaliseDefinition(definition, {
|
|
1056
|
-
engine: parsed.engine,
|
|
1057
|
-
relativePath,
|
|
1058
|
-
}),
|
|
1059
|
-
).filter(Boolean);
|
|
1060
|
-
if (definitions.length) {
|
|
1061
|
-
info.symbols = definitions;
|
|
1062
|
-
} else if (Array.isArray(parsed.symbols) && parsed.symbols.length) {
|
|
1063
|
-
info.symbols = parsed.symbols;
|
|
1064
|
-
}
|
|
1065
|
-
const deps =
|
|
1066
|
-
Array.isArray(parsed.dependencies) && parsed.dependencies.length
|
|
1067
|
-
? parsed.dependencies
|
|
1068
|
-
.map((dep) =>
|
|
1069
|
-
normaliseDependency(dep, {
|
|
1070
|
-
engine: parsed.engine,
|
|
1071
|
-
fromPath: relativePath,
|
|
1072
|
-
}),
|
|
1073
|
-
)
|
|
1074
|
-
.filter(Boolean)
|
|
1075
|
-
: [];
|
|
1076
|
-
if (deps.length) {
|
|
1077
|
-
info.dependencies = deps;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
if (!info.symbols.length) {
|
|
1083
|
-
info.symbols = extractSymbols(relativePath, content, language);
|
|
1084
|
-
}
|
|
1085
|
-
} catch (err) {
|
|
1086
|
-
logger.debug({ err, relativePath }, "Failed to extract symbols/dependencies for file");
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
fileInfos.push(info);
|
|
1091
|
-
} catch (err) {
|
|
1092
|
-
logger.warn({ err, relativePath }, "Failed to index file");
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const frameworks = detectFrameworks(fileInfos);
|
|
1097
|
-
const dependenciesRaw = fileInfos.flatMap((info) => {
|
|
1098
|
-
if (!Array.isArray(info.dependencies)) return [];
|
|
1099
|
-
return info.dependencies
|
|
1100
|
-
.filter((dep) => typeof dep?.to_path === "string" && dep.to_path.trim().length > 0)
|
|
1101
|
-
.map((dep) => ({
|
|
1102
|
-
from_path: dep.from_path ?? info.path,
|
|
1103
|
-
to_path: dep.to_path,
|
|
1104
|
-
kind: dep.kind ?? "reference",
|
|
1105
|
-
metadata: dep.metadata ?? null,
|
|
1106
|
-
}));
|
|
1107
|
-
});
|
|
1108
|
-
const dependenciesByFile = dependenciesRaw.reduce((acc, dep) => {
|
|
1109
|
-
const list = acc.get(dep.from_path) ?? [];
|
|
1110
|
-
list.push(dep);
|
|
1111
|
-
acc.set(dep.from_path, list);
|
|
1112
|
-
return acc;
|
|
1113
|
-
}, new Map());
|
|
1114
|
-
const summary = buildProjectSummary({ fileInfos, frameworks, dependenciesRaw });
|
|
1115
|
-
|
|
1116
|
-
const symbolIdRecords = [];
|
|
1117
|
-
|
|
1118
|
-
const initialInsertTx = db.transaction(() => {
|
|
1119
|
-
clearFilesStmt.run();
|
|
1120
|
-
clearSymbolsStmt.run();
|
|
1121
|
-
clearSymbolRefsStmt.run();
|
|
1122
|
-
clearFrameworkStmt.run();
|
|
1123
|
-
clearDependenciesStmt.run();
|
|
1124
|
-
|
|
1125
|
-
for (const info of fileInfos) {
|
|
1126
|
-
const record = { ...info };
|
|
1127
|
-
delete record.packageJson;
|
|
1128
|
-
const symbols = record.symbols ?? [];
|
|
1129
|
-
const dependencies = dependenciesByFile.get(record.path) ?? [];
|
|
1130
|
-
const content = fileContents.get(record.path);
|
|
1131
|
-
delete record.symbols;
|
|
1132
|
-
delete record.dependencies;
|
|
1133
|
-
insertFileStmt.run(record);
|
|
1134
|
-
if (symbols.length) {
|
|
1135
|
-
symbols.forEach((symbol) => {
|
|
1136
|
-
const result = insertSymbolStmt.run({
|
|
1137
|
-
file_path: record.path,
|
|
1138
|
-
name: symbol.name,
|
|
1139
|
-
kind: symbol.kind,
|
|
1140
|
-
line: symbol.line,
|
|
1141
|
-
column: symbol.column ?? 1,
|
|
1142
|
-
metadata: symbol.metadata ? JSON.stringify(symbol.metadata) : null,
|
|
1143
|
-
});
|
|
1144
|
-
symbolIdRecords.push({
|
|
1145
|
-
id: result.lastInsertRowid,
|
|
1146
|
-
name: symbol.name,
|
|
1147
|
-
filePath: record.path,
|
|
1148
|
-
line: symbol.line,
|
|
1149
|
-
column: symbol.column ?? 1,
|
|
1150
|
-
language: record.language,
|
|
1151
|
-
});
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
if (content !== undefined) {
|
|
1155
|
-
fileContents.set(record.path, content);
|
|
1156
|
-
}
|
|
1157
|
-
if (dependencies.length) {
|
|
1158
|
-
dependencies.forEach((dep) => {
|
|
1159
|
-
insertDependencyStmt.run({
|
|
1160
|
-
from_path: dep.from_path,
|
|
1161
|
-
to_path: dep.to_path,
|
|
1162
|
-
kind: dep.kind,
|
|
1163
|
-
metadata: dep.metadata ? JSON.stringify(dep.metadata) : null,
|
|
1164
|
-
});
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
for (const signal of frameworks.signals) {
|
|
1170
|
-
insertFrameworkStmt.run(signal);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
storeProjectSummary(summary);
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
initialInsertTx();
|
|
1177
|
-
|
|
1178
|
-
const defsByName = symbolIdRecords.reduce((acc, record) => {
|
|
1179
|
-
const list = acc.get(record.name) ?? [];
|
|
1180
|
-
list.push(record);
|
|
1181
|
-
acc.set(record.name, list);
|
|
1182
|
-
return acc;
|
|
1183
|
-
}, new Map());
|
|
1184
|
-
|
|
1185
|
-
const referenceTx = db.transaction(() => {
|
|
1186
|
-
navigationData.forEach((nav, filePath) => {
|
|
1187
|
-
const references = Array.isArray(nav.references) ? nav.references : [];
|
|
1188
|
-
const definitions = Array.isArray(nav.definitions) ? nav.definitions : [];
|
|
1189
|
-
const referenceMap = new Map();
|
|
1190
|
-
references.forEach((ref) => {
|
|
1191
|
-
if (!ref || typeof ref.name !== "string") return;
|
|
1192
|
-
const name = ref.name.trim();
|
|
1193
|
-
if (!name) return;
|
|
1194
|
-
const line = Number.isFinite(ref.line) ? Math.trunc(ref.line) : null;
|
|
1195
|
-
const column = Number.isFinite(ref.column) ? Math.trunc(ref.column) : null;
|
|
1196
|
-
if (!line || line <= 0) return;
|
|
1197
|
-
const key = `${name}:${line}:${column ?? 0}`;
|
|
1198
|
-
referenceMap.set(key, {
|
|
1199
|
-
name,
|
|
1200
|
-
line,
|
|
1201
|
-
column: column ?? null,
|
|
1202
|
-
snippet: typeof ref.snippet === "string" ? ref.snippet : null,
|
|
1203
|
-
metadata:
|
|
1204
|
-
typeof ref.metadata === "object" && ref.metadata !== null
|
|
1205
|
-
? { ...ref.metadata }
|
|
1206
|
-
: {},
|
|
1207
|
-
});
|
|
1208
|
-
});
|
|
1209
|
-
|
|
1210
|
-
definitions.forEach((definition) => {
|
|
1211
|
-
if (!definition || typeof definition.name !== "string") return;
|
|
1212
|
-
const name = definition.name.trim();
|
|
1213
|
-
if (!name) return;
|
|
1214
|
-
const defs = defsByName.get(name);
|
|
1215
|
-
if (!defs || !defs.length) return;
|
|
1216
|
-
referenceMap.forEach((ref) => {
|
|
1217
|
-
if (ref.name !== name) return;
|
|
1218
|
-
defs.forEach((def) => {
|
|
1219
|
-
if (def.filePath === filePath && def.line === ref.line && def.column === ref.column) {
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
const metadata = {
|
|
1223
|
-
engine: nav.engine ?? "tree_sitter",
|
|
1224
|
-
language: def.language ?? null,
|
|
1225
|
-
...ref.metadata,
|
|
1226
|
-
};
|
|
1227
|
-
insertSymbolReferenceStmt.run({
|
|
1228
|
-
symbol_id: def.id,
|
|
1229
|
-
file_path: filePath,
|
|
1230
|
-
line: ref.line,
|
|
1231
|
-
column: ref.column ?? null,
|
|
1232
|
-
snippet: ref.snippet,
|
|
1233
|
-
metadata: JSON.stringify(metadata),
|
|
1234
|
-
});
|
|
1235
|
-
referenceCount += 1;
|
|
1236
|
-
});
|
|
1237
|
-
});
|
|
1238
|
-
});
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
fileContents.forEach((content, filePath) => {
|
|
1242
|
-
if (navigationData.has(filePath)) return;
|
|
1243
|
-
if (typeof content !== "string" || content.length === 0) return;
|
|
1244
|
-
const lines = content.split(/\r?\n/);
|
|
1245
|
-
defsByName.forEach((defs, symbolName) => {
|
|
1246
|
-
const regex = getCachedRegex(symbolName);
|
|
1247
|
-
lines.some((line, lineIndex) => {
|
|
1248
|
-
if (referenceCount >= FALLBACK_REFERENCE_SAMPLE_LIMIT) {
|
|
1249
|
-
return true;
|
|
1250
|
-
}
|
|
1251
|
-
let match;
|
|
1252
|
-
while ((match = regex.exec(line)) !== null) {
|
|
1253
|
-
const column = match.index + 1;
|
|
1254
|
-
const snippet = line.trim();
|
|
1255
|
-
defs.forEach((def) => {
|
|
1256
|
-
if (
|
|
1257
|
-
def.filePath === filePath &&
|
|
1258
|
-
def.line === lineIndex + 1 &&
|
|
1259
|
-
def.column === column
|
|
1260
|
-
) {
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
if (referenceCount >= FALLBACK_REFERENCE_SAMPLE_LIMIT) return;
|
|
1264
|
-
insertSymbolReferenceStmt.run({
|
|
1265
|
-
symbol_id: def.id,
|
|
1266
|
-
file_path: filePath,
|
|
1267
|
-
line: lineIndex + 1,
|
|
1268
|
-
column,
|
|
1269
|
-
snippet,
|
|
1270
|
-
metadata: JSON.stringify({
|
|
1271
|
-
language: def.language,
|
|
1272
|
-
engine: "heuristic",
|
|
1273
|
-
}),
|
|
1274
|
-
});
|
|
1275
|
-
referenceCount += 1;
|
|
1276
|
-
});
|
|
1277
|
-
if (referenceCount >= FALLBACK_REFERENCE_SAMPLE_LIMIT) {
|
|
1278
|
-
return true;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
return referenceCount >= FALLBACK_REFERENCE_SAMPLE_LIMIT;
|
|
1282
|
-
});
|
|
1283
|
-
});
|
|
1284
|
-
});
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
referenceTx();
|
|
1288
|
-
|
|
1289
|
-
logger.info(
|
|
1290
|
-
{
|
|
1291
|
-
fileCount: fileInfos.length,
|
|
1292
|
-
frameworks: summary.frameworks,
|
|
1293
|
-
languages: summary.languages,
|
|
1294
|
-
},
|
|
1295
|
-
"Workspace index rebuild complete",
|
|
1296
|
-
);
|
|
1297
|
-
try {
|
|
1298
|
-
await ensureClaudeDoc(summary);
|
|
1299
|
-
} catch (err) {
|
|
1300
|
-
logger.warn({ err }, "Failed to update CLAUDE.md");
|
|
1301
|
-
}
|
|
1302
|
-
return summary;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
function getProjectSummary() {
|
|
1306
|
-
const summary = readProjectSummary();
|
|
1307
|
-
if (summary) return summary;
|
|
1308
|
-
return {
|
|
1309
|
-
indexedAt: null,
|
|
1310
|
-
workspaceRoot,
|
|
1311
|
-
fileCount: 0,
|
|
1312
|
-
languages: [],
|
|
1313
|
-
frameworks: [],
|
|
1314
|
-
frameworkSignals: [],
|
|
1315
|
-
message: "No project summary found. Run workspace_index_rebuild to generate one.",
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
async function fallbackSearch({ query, regex, limit, ignore }) {
|
|
1320
|
-
const patterns = ["**/*"];
|
|
1321
|
-
const entries = await fg(patterns, {
|
|
1322
|
-
cwd: workspaceRoot,
|
|
1323
|
-
ignore,
|
|
1324
|
-
onlyFiles: true,
|
|
1325
|
-
unique: true,
|
|
1326
|
-
followSymbolicLinks: false,
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1329
|
-
const matches = [];
|
|
1330
|
-
const matcher = regex ? new RegExp(query, "g") : null;
|
|
1331
|
-
|
|
1332
|
-
for (const entry of entries) {
|
|
1333
|
-
if (matches.length >= limit) break;
|
|
1334
|
-
const content = await readFileExcerpt(entry);
|
|
1335
|
-
if (!content) continue;
|
|
1336
|
-
|
|
1337
|
-
const lines = content.split("\n");
|
|
1338
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
1339
|
-
if (matches.length >= limit) break;
|
|
1340
|
-
const line = lines[i];
|
|
1341
|
-
if (regex) {
|
|
1342
|
-
if (matcher.test(line)) {
|
|
1343
|
-
matches.push({
|
|
1344
|
-
path: entry,
|
|
1345
|
-
line: i + 1,
|
|
1346
|
-
column: null,
|
|
1347
|
-
match: line,
|
|
1348
|
-
});
|
|
1349
|
-
matcher.lastIndex = 0;
|
|
1350
|
-
}
|
|
1351
|
-
} else if (line.includes(query)) {
|
|
1352
|
-
matches.push({
|
|
1353
|
-
path: entry,
|
|
1354
|
-
line: i + 1,
|
|
1355
|
-
column: line.indexOf(query),
|
|
1356
|
-
match: line,
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
return matches;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
async function searchWorkspace(options = {}) {
|
|
1366
|
-
const query = options.query ?? options.term ?? options.pattern;
|
|
1367
|
-
if (typeof query !== "string" || query.trim().length === 0) {
|
|
1368
|
-
throw new Error("Search query must be a non-empty string.");
|
|
1369
|
-
}
|
|
1370
|
-
const trimmedQuery = query.trim();
|
|
1371
|
-
const regex = options.regex === true || options.isRegex === true;
|
|
1372
|
-
const limit =
|
|
1373
|
-
Number.isInteger(options.limit) && options.limit > 0
|
|
1374
|
-
? Math.min(options.limit, MAX_RESULTS)
|
|
1375
|
-
: 50;
|
|
1376
|
-
const ignore = normaliseIgnore(options.ignore);
|
|
1377
|
-
|
|
1378
|
-
try {
|
|
1379
|
-
const results = await searchWithRipgrep({
|
|
1380
|
-
query: trimmedQuery,
|
|
1381
|
-
regex,
|
|
1382
|
-
limit,
|
|
1383
|
-
ignore,
|
|
1384
|
-
});
|
|
1385
|
-
return {
|
|
1386
|
-
engine: "ripgrep",
|
|
1387
|
-
query: trimmedQuery,
|
|
1388
|
-
regex,
|
|
1389
|
-
limit,
|
|
1390
|
-
matches: results,
|
|
1391
|
-
};
|
|
1392
|
-
} catch (err) {
|
|
1393
|
-
logger.warn({ err }, "ripgrep search failed, falling back to Node search");
|
|
1394
|
-
const results = await fallbackSearch({
|
|
1395
|
-
query: trimmedQuery,
|
|
1396
|
-
regex,
|
|
1397
|
-
limit,
|
|
1398
|
-
ignore,
|
|
1399
|
-
});
|
|
1400
|
-
return {
|
|
1401
|
-
engine: "fallback",
|
|
1402
|
-
query: trimmedQuery,
|
|
1403
|
-
regex,
|
|
1404
|
-
limit,
|
|
1405
|
-
matches: results,
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
function searchSymbols(options = {}) {
|
|
1411
|
-
const query = options.query ?? options.name ?? options.symbol;
|
|
1412
|
-
if (typeof query !== "string" || query.trim().length === 0) {
|
|
1413
|
-
throw new Error("Symbol query must be a non-empty string.");
|
|
1414
|
-
}
|
|
1415
|
-
const trimmedQuery = query.trim();
|
|
1416
|
-
const limit =
|
|
1417
|
-
Number.isInteger(options.limit) && options.limit > 0
|
|
1418
|
-
? Math.min(options.limit, MAX_RESULTS)
|
|
1419
|
-
: 50;
|
|
1420
|
-
const language = options.language ? String(options.language).toLowerCase() : null;
|
|
1421
|
-
const filePath =
|
|
1422
|
-
typeof options.path === "string"
|
|
1423
|
-
? options.path
|
|
1424
|
-
: typeof options.file === "string"
|
|
1425
|
-
? options.file
|
|
1426
|
-
: null;
|
|
1427
|
-
|
|
1428
|
-
let sql = `SELECT s.file_path,
|
|
1429
|
-
s.name,
|
|
1430
|
-
s.kind,
|
|
1431
|
-
s.line,
|
|
1432
|
-
s.column,
|
|
1433
|
-
s.metadata,
|
|
1434
|
-
f.language
|
|
1435
|
-
FROM symbols s
|
|
1436
|
-
LEFT JOIN files f ON s.file_path = f.path
|
|
1437
|
-
WHERE s.name LIKE ?`;
|
|
1438
|
-
const params = [`%${trimmedQuery}%`];
|
|
1439
|
-
|
|
1440
|
-
if (language) {
|
|
1441
|
-
sql += " AND (f.language = ? OR LOWER(f.language) = ?)";
|
|
1442
|
-
params.push(language, language);
|
|
1443
|
-
}
|
|
1444
|
-
if (filePath) {
|
|
1445
|
-
sql += " AND s.file_path = ?";
|
|
1446
|
-
params.push(filePath);
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
sql += " ORDER BY s.name ASC, s.line ASC LIMIT ?";
|
|
1450
|
-
params.push(limit);
|
|
1451
|
-
|
|
1452
|
-
const rows = db.prepare(sql).all(...params);
|
|
1453
|
-
return rows.map((row) => ({
|
|
1454
|
-
filePath: row.file_path,
|
|
1455
|
-
name: row.name,
|
|
1456
|
-
kind: row.kind,
|
|
1457
|
-
line: row.line,
|
|
1458
|
-
column: row.column ?? null,
|
|
1459
|
-
language: row.language ?? null,
|
|
1460
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
1461
|
-
}));
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
function searchSymbolReferences(options = {}) {
|
|
1465
|
-
const query = options.symbol ?? options.name ?? options.query;
|
|
1466
|
-
if (typeof query !== "string" || query.trim().length === 0) {
|
|
1467
|
-
throw new Error("Symbol reference query must be a non-empty string.");
|
|
1468
|
-
}
|
|
1469
|
-
const trimmedQuery = query.trim();
|
|
1470
|
-
const limit =
|
|
1471
|
-
Number.isInteger(options.limit) && options.limit > 0
|
|
1472
|
-
? Math.min(options.limit, MAX_RESULTS)
|
|
1473
|
-
: 100;
|
|
1474
|
-
const filePath =
|
|
1475
|
-
typeof options.path === "string"
|
|
1476
|
-
? options.path
|
|
1477
|
-
: typeof options.file === "string"
|
|
1478
|
-
? options.file
|
|
1479
|
-
: undefined;
|
|
1480
|
-
|
|
1481
|
-
let sql = `SELECT s.name,
|
|
1482
|
-
s.kind,
|
|
1483
|
-
s.file_path AS definition_path,
|
|
1484
|
-
s.line AS definition_line,
|
|
1485
|
-
s.column AS definition_column,
|
|
1486
|
-
r.file_path,
|
|
1487
|
-
r.line,
|
|
1488
|
-
r.column,
|
|
1489
|
-
r.snippet,
|
|
1490
|
-
r.metadata,
|
|
1491
|
-
f.language
|
|
1492
|
-
FROM symbol_references r
|
|
1493
|
-
JOIN symbols s ON r.symbol_id = s.id
|
|
1494
|
-
LEFT JOIN files f ON s.file_path = f.path
|
|
1495
|
-
WHERE s.name LIKE ?`;
|
|
1496
|
-
const params = [`%${trimmedQuery}%`];
|
|
1497
|
-
|
|
1498
|
-
if (filePath) {
|
|
1499
|
-
sql += " AND r.file_path = ?";
|
|
1500
|
-
params.push(filePath);
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
sql += " ORDER BY s.name ASC, r.file_path ASC, r.line ASC LIMIT ?";
|
|
1504
|
-
params.push(limit);
|
|
1505
|
-
|
|
1506
|
-
const rows = db.prepare(sql).all(...params);
|
|
1507
|
-
const parsed = rows.map((row) => {
|
|
1508
|
-
const symbolMetadata = row.symbol_metadata ? safeParseJson(row.symbol_metadata) : null;
|
|
1509
|
-
const referenceMetadata = row.reference_metadata ? safeParseJson(row.reference_metadata) : null;
|
|
1510
|
-
const engine = referenceMetadata?.engine ?? symbolMetadata?.engine ?? null;
|
|
1511
|
-
return {
|
|
1512
|
-
symbol: row.name,
|
|
1513
|
-
kind: row.kind,
|
|
1514
|
-
definition: {
|
|
1515
|
-
filePath: row.definition_path,
|
|
1516
|
-
line: row.definition_line,
|
|
1517
|
-
column: row.definition_column,
|
|
1518
|
-
},
|
|
1519
|
-
reference: {
|
|
1520
|
-
filePath: row.file_path,
|
|
1521
|
-
line: row.line,
|
|
1522
|
-
column: row.column ?? null,
|
|
1523
|
-
snippet: row.snippet ?? null,
|
|
1524
|
-
},
|
|
1525
|
-
language: row.language ?? null,
|
|
1526
|
-
engine,
|
|
1527
|
-
metadata: {
|
|
1528
|
-
symbol: symbolMetadata,
|
|
1529
|
-
reference: referenceMetadata,
|
|
1530
|
-
},
|
|
1531
|
-
};
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
parsed.sort((a, b) => {
|
|
1535
|
-
const priority = (engine) => {
|
|
1536
|
-
if (!engine) return 10;
|
|
1537
|
-
return engine === "tree_sitter" ? 0 : engine === "heuristic" ? 5 : 3;
|
|
1538
|
-
};
|
|
1539
|
-
const diff = priority(a.engine) - priority(b.engine);
|
|
1540
|
-
if (diff !== 0) return diff;
|
|
1541
|
-
if (a.reference.filePath !== b.reference.filePath) {
|
|
1542
|
-
return a.reference.filePath.localeCompare(b.reference.filePath);
|
|
1543
|
-
}
|
|
1544
|
-
return (a.reference.line ?? 0) - (b.reference.line ?? 0);
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
return parsed;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function formatGotoDefinitionResult(row) {
|
|
1551
|
-
const referenceMeta = row.metadata ? safeParseJson(row.metadata, {}) : {};
|
|
1552
|
-
return {
|
|
1553
|
-
symbol: row.name,
|
|
1554
|
-
kind: row.kind,
|
|
1555
|
-
definition: {
|
|
1556
|
-
filePath: row.definition_path,
|
|
1557
|
-
line: row.definition_line,
|
|
1558
|
-
column: row.definition_column,
|
|
1559
|
-
},
|
|
1560
|
-
reference: {
|
|
1561
|
-
filePath: row.reference_path,
|
|
1562
|
-
line: row.reference_line,
|
|
1563
|
-
column: row.reference_column ?? null,
|
|
1564
|
-
snippet: row.snippet ?? null,
|
|
1565
|
-
},
|
|
1566
|
-
engine: referenceMeta.engine ?? null,
|
|
1567
|
-
metadata: referenceMeta,
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
function findDefinitionNearLocation({ filePath, line, column, limit = 10 }) {
|
|
1572
|
-
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
1573
|
-
throw new Error("go to definition requires a file path.");
|
|
1574
|
-
}
|
|
1575
|
-
if (!Number.isInteger(line) || line <= 0) {
|
|
1576
|
-
throw new Error("go to definition requires a positive line number.");
|
|
1577
|
-
}
|
|
1578
|
-
const params = {
|
|
1579
|
-
filePath,
|
|
1580
|
-
line,
|
|
1581
|
-
column: Number.isInteger(column) && column > 0 ? column : null,
|
|
1582
|
-
limit: Math.min(limit, 20),
|
|
1583
|
-
};
|
|
1584
|
-
const rows = selectDefinitionByLocationStmt.all(params).map(formatGotoDefinitionResult);
|
|
1585
|
-
if (!rows.length && column !== null) {
|
|
1586
|
-
params.column = null;
|
|
1587
|
-
return selectDefinitionByLocationStmt.all(params).map(formatGotoDefinitionResult);
|
|
1588
|
-
}
|
|
1589
|
-
const preferred = rows.filter((row) => row.engine === "tree_sitter");
|
|
1590
|
-
return preferred.length ? preferred : rows;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
function listDefinitionsBySymbol({ name, limit = 50 }) {
|
|
1594
|
-
if (typeof name !== "string" || name.trim().length === 0) {
|
|
1595
|
-
throw new Error("Definition query requires a symbol name.");
|
|
1596
|
-
}
|
|
1597
|
-
const rows = selectDefinitionsBySymbolStmt.all({
|
|
1598
|
-
name,
|
|
1599
|
-
limit: Math.min(limit, 200),
|
|
1600
|
-
});
|
|
1601
|
-
return rows
|
|
1602
|
-
.map((row) => ({
|
|
1603
|
-
name: row.name,
|
|
1604
|
-
kind: row.kind,
|
|
1605
|
-
filePath: row.file_path,
|
|
1606
|
-
line: row.line,
|
|
1607
|
-
column: row.column ?? null,
|
|
1608
|
-
metadata: safeParseJson(row.metadata, null),
|
|
1609
|
-
}))
|
|
1610
|
-
.sort((a, b) => {
|
|
1611
|
-
const aEngine = a.metadata?.engine ?? null;
|
|
1612
|
-
const bEngine = b.metadata?.engine ?? null;
|
|
1613
|
-
if (aEngine === bEngine) return 0;
|
|
1614
|
-
if (aEngine === "tree_sitter") return -1;
|
|
1615
|
-
if (bEngine === "tree_sitter") return 1;
|
|
1616
|
-
return 0;
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
module.exports = {
|
|
1621
|
-
listWorkspaceFiles,
|
|
1622
|
-
searchWorkspace,
|
|
1623
|
-
rebuildWorkspaceIndex,
|
|
1624
|
-
getProjectSummary,
|
|
1625
|
-
searchSymbols,
|
|
1626
|
-
searchSymbolReferences,
|
|
1627
|
-
findDefinitionNearLocation,
|
|
1628
|
-
listDefinitionsBySymbol,
|
|
1629
|
-
};
|