lynkr 9.9.1 → 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.
Files changed (67) hide show
  1. package/README.md +50 -9
  2. package/bin/cli.js +2 -0
  3. package/bin/lynkr-init.js +4 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +2 -2
  7. package/scripts/validate-difficulty-classifier.js +27 -6
  8. package/src/api/providers-handler.js +0 -1
  9. package/src/api/router.js +275 -160
  10. package/src/clients/databricks.js +95 -6
  11. package/src/config/index.js +3 -43
  12. package/src/orchestrator/index.js +117 -1235
  13. package/src/orchestrator/passthrough-stream.js +382 -0
  14. package/src/orchestrator/sse-transformer.js +408 -0
  15. package/src/routing/affinity-store.js +17 -3
  16. package/src/routing/difficulty-classifier.js +52 -10
  17. package/src/routing/index.js +35 -3
  18. package/src/routing/intent-score.js +20 -2
  19. package/src/routing/session-affinity.js +8 -1
  20. package/src/routing/side-channel-detector.js +103 -0
  21. package/src/server.js +0 -46
  22. package/src/agents/context-manager.js +0 -236
  23. package/src/agents/decomposition/dispatcher.js +0 -185
  24. package/src/agents/decomposition/gate.js +0 -136
  25. package/src/agents/decomposition/index.js +0 -183
  26. package/src/agents/decomposition/model-call.js +0 -75
  27. package/src/agents/decomposition/planner.js +0 -223
  28. package/src/agents/decomposition/synthesizer.js +0 -89
  29. package/src/agents/decomposition/telemetry.js +0 -55
  30. package/src/agents/definitions/loader.js +0 -653
  31. package/src/agents/executor.js +0 -457
  32. package/src/agents/index.js +0 -165
  33. package/src/agents/parallel-coordinator.js +0 -68
  34. package/src/agents/reflector.js +0 -331
  35. package/src/agents/skillbook.js +0 -331
  36. package/src/agents/store.js +0 -259
  37. package/src/edits/index.js +0 -171
  38. package/src/indexer/babel-parser.js +0 -213
  39. package/src/indexer/index.js +0 -1629
  40. package/src/indexer/navigation/index.js +0 -32
  41. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  42. package/src/indexer/parser.js +0 -443
  43. package/src/tasks/store.js +0 -349
  44. package/src/tests/coverage.js +0 -173
  45. package/src/tests/index.js +0 -171
  46. package/src/tests/store.js +0 -213
  47. package/src/tools/agent-task.js +0 -145
  48. package/src/tools/code-mode.js +0 -304
  49. package/src/tools/decompose.js +0 -91
  50. package/src/tools/edits.js +0 -94
  51. package/src/tools/execution.js +0 -171
  52. package/src/tools/git.js +0 -1346
  53. package/src/tools/index.js +0 -306
  54. package/src/tools/indexer.js +0 -360
  55. package/src/tools/lazy-loader.js +0 -366
  56. package/src/tools/mcp-remote.js +0 -88
  57. package/src/tools/mcp.js +0 -116
  58. package/src/tools/process.js +0 -167
  59. package/src/tools/smart-selection.js +0 -180
  60. package/src/tools/stubs.js +0 -55
  61. package/src/tools/tasks.js +0 -260
  62. package/src/tools/tests.js +0 -132
  63. package/src/tools/tinyfish.js +0 -358
  64. package/src/tools/truncate.js +0 -106
  65. package/src/tools/web-client.js +0 -71
  66. package/src/tools/web.js +0 -415
  67. package/src/tools/workspace.js +0 -204
@@ -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
- };