@yuaone/core 0.1.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/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module codebase-context
|
|
3
|
+
* @description Codebase Context Engine — indexes TypeScript/JavaScript projects,
|
|
4
|
+
* extracts symbols, builds call graphs, and provides semantic search + blast radius analysis.
|
|
5
|
+
*
|
|
6
|
+
* Uses regex-based AST analysis (no ts-morph dependency). Designed for the YUAN coding agent
|
|
7
|
+
* to understand project structure before making changes.
|
|
8
|
+
*/
|
|
9
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
10
|
+
import { join, resolve, extname, relative } from "node:path";
|
|
11
|
+
// ─── Constants ───
|
|
12
|
+
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
13
|
+
const SKIP_DIRS = new Set([
|
|
14
|
+
"node_modules", "dist", ".git", "build", "coverage",
|
|
15
|
+
".next", ".turbo", "__pycache__", ".cache", ".output",
|
|
16
|
+
]);
|
|
17
|
+
const TEST_FILE_PATTERNS = [
|
|
18
|
+
/\.test\.[tj]sx?$/,
|
|
19
|
+
/\.spec\.[tj]sx?$/,
|
|
20
|
+
/__tests__\//,
|
|
21
|
+
/\.stories\.[tj]sx?$/,
|
|
22
|
+
];
|
|
23
|
+
// ─── Regex patterns for symbol extraction ───
|
|
24
|
+
const FUNCTION_RE = /^(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:default\s+)?)?(?:declare\s+)?(async\s+)?function\s*\*?\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/gm;
|
|
25
|
+
const ARROW_FUNCTION_RE = /^(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:default\s+)?)?(const|let)\s+(\w+)\s*(?::\s*[^=]+?)?\s*=\s*(async\s+)?(?:\([^)]*\)|[^=>\s]+)\s*(?::\s*[^=]+?)?\s*=>/gm;
|
|
26
|
+
const CLASS_RE = /^(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:default\s+)?)?(?:declare\s+)?(abstract\s+)?class\s+(\w+)(?:\s*<[^>]*>)?(?:\s+extends\s+(\w+(?:<[^>]*>)?))?(?:\s+implements\s+([^{]+))?\s*\{/gm;
|
|
27
|
+
const INTERFACE_RE = /^(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:default\s+)?)?interface\s+(\w+)(?:\s*<[^>]*>)?(?:\s+extends\s+([^{]+))?\s*\{/gm;
|
|
28
|
+
const TYPE_ALIAS_RE = /^(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:default\s+)?)?type\s+(\w+)(?:\s*<[^>]*>)?\s*=/gm;
|
|
29
|
+
const ENUM_RE = /^(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:default\s+)?)?(const\s+)?enum\s+(\w+)\s*\{/gm;
|
|
30
|
+
const METHOD_RE = /^\s*(?:\/\*\*[\s\S]*?\*\/\s*)?(public|private|protected)?\s*(static\s+)?(async\s+)?(get|set)?\s*(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?\s*[{;]/gm;
|
|
31
|
+
// Import patterns (reused from dependency-analyzer style)
|
|
32
|
+
const IMPORT_RE = /import\s+(?:type\s+)?(?:\{([^}]*)\}|(\w+)|\*\s+as\s+(\w+))\s+from\s+["']([^"']+)["']/g;
|
|
33
|
+
const IMPORT_SIDE_EFFECT_RE = /import\s+["']([^"']+)["']/g;
|
|
34
|
+
const RE_EXPORT_RE = /export\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']([^"']+)["']/g;
|
|
35
|
+
const EXPORT_NAMED_RE = /export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?|class|const|let|var|interface|type|enum)\s+(\w+)/g;
|
|
36
|
+
const EXPORT_DEFAULT_RE = /export\s+default\s+/g;
|
|
37
|
+
const EXPORT_LIST_RE = /export\s+\{([^}]*)\}(?!\s*from)/g;
|
|
38
|
+
// Call site detection (function calls within bodies)
|
|
39
|
+
const CALL_SITE_RE = /\b(\w+)\s*(?:<[^>]*>)?\s*\(/g;
|
|
40
|
+
// Built-in identifiers to skip in call graph
|
|
41
|
+
const BUILTIN_CALLS = new Set([
|
|
42
|
+
"if", "for", "while", "switch", "catch", "return", "throw", "new", "typeof",
|
|
43
|
+
"instanceof", "delete", "void", "await", "yield", "import", "require",
|
|
44
|
+
"console", "Math", "JSON", "Object", "Array", "String", "Number", "Boolean",
|
|
45
|
+
"Date", "RegExp", "Error", "Map", "Set", "Promise", "Symbol", "parseInt",
|
|
46
|
+
"parseFloat", "isNaN", "isFinite", "setTimeout", "setInterval", "clearTimeout",
|
|
47
|
+
"clearInterval", "fetch", "URL", "Buffer", "process",
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Codebase Context Engine — indexes TypeScript/JavaScript projects and provides
|
|
51
|
+
* symbol lookup, call graph analysis, blast radius estimation, and semantic search.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const ctx = new CodebaseContext("/path/to/project");
|
|
56
|
+
* await ctx.buildIndex();
|
|
57
|
+
*
|
|
58
|
+
* const symbols = ctx.findSymbol("handleRequest");
|
|
59
|
+
* const blast = ctx.getBlastRadius("/path/to/project/src/router.ts");
|
|
60
|
+
* const results = ctx.searchSymbols("auth middleware", 5);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export class CodebaseContext {
|
|
64
|
+
index;
|
|
65
|
+
projectPath;
|
|
66
|
+
/** Cached file contents for snippet extraction */
|
|
67
|
+
fileContents = new Map();
|
|
68
|
+
/** Reverse dependency map (file → files that import it) */
|
|
69
|
+
reverseDepMap = new Map();
|
|
70
|
+
/** Optional multi-language support for non-TS/JS files */
|
|
71
|
+
languageSupport;
|
|
72
|
+
constructor(projectPath, languageSupport) {
|
|
73
|
+
this.projectPath = resolve(projectPath);
|
|
74
|
+
this.languageSupport = languageSupport ?? null;
|
|
75
|
+
this.index = {
|
|
76
|
+
files: new Map(),
|
|
77
|
+
symbolTable: new Map(),
|
|
78
|
+
callGraph: [],
|
|
79
|
+
lastIndexedAt: 0,
|
|
80
|
+
totalFiles: 0,
|
|
81
|
+
totalSymbols: 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ─── Indexing ───
|
|
85
|
+
/**
|
|
86
|
+
* Build a full index of the project. Scans all source files,
|
|
87
|
+
* extracts symbols, builds call graph, and creates the symbol table.
|
|
88
|
+
*
|
|
89
|
+
* @returns The complete codebase index
|
|
90
|
+
*/
|
|
91
|
+
async buildIndex() {
|
|
92
|
+
const files = await this.collectSourceFiles(this.projectPath);
|
|
93
|
+
this.index.files.clear();
|
|
94
|
+
this.index.symbolTable.clear();
|
|
95
|
+
this.index.callGraph = [];
|
|
96
|
+
this.fileContents.clear();
|
|
97
|
+
this.reverseDepMap.clear();
|
|
98
|
+
// Analyze all files
|
|
99
|
+
for (const filePath of files) {
|
|
100
|
+
try {
|
|
101
|
+
const content = await readFile(filePath, "utf-8");
|
|
102
|
+
this.fileContents.set(filePath, content);
|
|
103
|
+
const analysis = this.analyzeFile(filePath, content);
|
|
104
|
+
this.index.files.set(filePath, analysis);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Skip unreadable files
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Build symbol table and call graph
|
|
111
|
+
this.buildSymbolTable();
|
|
112
|
+
this.buildCallGraph();
|
|
113
|
+
this.buildReverseDependencyMap();
|
|
114
|
+
this.index.lastIndexedAt = Date.now();
|
|
115
|
+
this.index.totalFiles = this.index.files.size;
|
|
116
|
+
this.index.totalSymbols = Array.from(this.index.symbolTable.values())
|
|
117
|
+
.reduce((sum, arr) => sum + arr.length, 0);
|
|
118
|
+
return this.index;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Incrementally update the index for a single changed file.
|
|
122
|
+
* Re-analyzes only that file and updates the symbol table.
|
|
123
|
+
*
|
|
124
|
+
* @param filePath - Absolute path of the changed file
|
|
125
|
+
*/
|
|
126
|
+
async updateFile(filePath) {
|
|
127
|
+
const absPath = resolve(filePath);
|
|
128
|
+
try {
|
|
129
|
+
const content = await readFile(absPath, "utf-8");
|
|
130
|
+
this.fileContents.set(absPath, content);
|
|
131
|
+
const analysis = this.analyzeFile(absPath, content);
|
|
132
|
+
this.index.files.set(absPath, analysis);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// File was deleted or unreadable — remove it
|
|
136
|
+
this.removeFile(absPath);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Rebuild symbol table and call graph
|
|
140
|
+
this.buildSymbolTable();
|
|
141
|
+
this.buildCallGraph();
|
|
142
|
+
this.buildReverseDependencyMap();
|
|
143
|
+
this.index.lastIndexedAt = Date.now();
|
|
144
|
+
this.index.totalFiles = this.index.files.size;
|
|
145
|
+
this.index.totalSymbols = Array.from(this.index.symbolTable.values())
|
|
146
|
+
.reduce((sum, arr) => sum + arr.length, 0);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Remove a file from the index (e.g., after deletion).
|
|
150
|
+
*
|
|
151
|
+
* @param filePath - Absolute path of the removed file
|
|
152
|
+
*/
|
|
153
|
+
removeFile(filePath) {
|
|
154
|
+
const absPath = resolve(filePath);
|
|
155
|
+
this.index.files.delete(absPath);
|
|
156
|
+
this.fileContents.delete(absPath);
|
|
157
|
+
// Rebuild derived structures
|
|
158
|
+
this.buildSymbolTable();
|
|
159
|
+
this.buildCallGraph();
|
|
160
|
+
this.buildReverseDependencyMap();
|
|
161
|
+
this.index.totalFiles = this.index.files.size;
|
|
162
|
+
this.index.totalSymbols = Array.from(this.index.symbolTable.values())
|
|
163
|
+
.reduce((sum, arr) => sum + arr.length, 0);
|
|
164
|
+
}
|
|
165
|
+
// ─── Symbol queries ───
|
|
166
|
+
/**
|
|
167
|
+
* Find all symbols matching the given name across the codebase.
|
|
168
|
+
*
|
|
169
|
+
* @param name - Symbol name to search for (exact match)
|
|
170
|
+
* @returns Array of matching SymbolInfo
|
|
171
|
+
*/
|
|
172
|
+
findSymbol(name) {
|
|
173
|
+
return this.index.symbolTable.get(name) ?? [];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Find the symbol defined at a specific file and line.
|
|
177
|
+
*
|
|
178
|
+
* @param file - Absolute file path
|
|
179
|
+
* @param line - Line number (1-based)
|
|
180
|
+
* @returns The symbol at that location, or undefined
|
|
181
|
+
*/
|
|
182
|
+
findSymbolAt(file, line) {
|
|
183
|
+
const absFile = resolve(file);
|
|
184
|
+
const analysis = this.index.files.get(absFile);
|
|
185
|
+
if (!analysis)
|
|
186
|
+
return undefined;
|
|
187
|
+
// Find the most specific (innermost) symbol containing this line
|
|
188
|
+
let best;
|
|
189
|
+
for (const sym of analysis.symbols) {
|
|
190
|
+
if (line >= sym.line && line <= sym.endLine) {
|
|
191
|
+
if (!best || (sym.endLine - sym.line) < (best.endLine - best.line)) {
|
|
192
|
+
best = sym;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Also check members
|
|
196
|
+
if (sym.members) {
|
|
197
|
+
for (const member of sym.members) {
|
|
198
|
+
if (line >= member.line && line <= member.endLine) {
|
|
199
|
+
if (!best || (member.endLine - member.line) < (best.endLine - best.line)) {
|
|
200
|
+
best = member;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return best;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Find all symbols of a given kind across the codebase.
|
|
210
|
+
*
|
|
211
|
+
* @param kind - Symbol kind to filter by
|
|
212
|
+
* @returns Array of matching SymbolInfo
|
|
213
|
+
*/
|
|
214
|
+
findSymbolsByKind(kind) {
|
|
215
|
+
const results = [];
|
|
216
|
+
for (const symbols of this.index.symbolTable.values()) {
|
|
217
|
+
for (const sym of symbols) {
|
|
218
|
+
if (sym.kind === kind)
|
|
219
|
+
results.push(sym);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Find all exported symbols in a specific file.
|
|
226
|
+
*
|
|
227
|
+
* @param file - Absolute file path
|
|
228
|
+
* @returns Array of exported SymbolInfo
|
|
229
|
+
*/
|
|
230
|
+
findExportedSymbols(file) {
|
|
231
|
+
const absFile = resolve(file);
|
|
232
|
+
const analysis = this.index.files.get(absFile);
|
|
233
|
+
if (!analysis)
|
|
234
|
+
return [];
|
|
235
|
+
return analysis.symbols.filter((s) => s.exported);
|
|
236
|
+
}
|
|
237
|
+
// ─── Relationship queries ───
|
|
238
|
+
/**
|
|
239
|
+
* Get all call edges where the given symbol is the callee.
|
|
240
|
+
*
|
|
241
|
+
* @param symbolName - Name of the called function/method
|
|
242
|
+
* @param file - Optional file path to narrow the search
|
|
243
|
+
* @returns Call edges targeting this symbol
|
|
244
|
+
*/
|
|
245
|
+
getCallersOf(symbolName, file) {
|
|
246
|
+
const suffix = file ? `${resolve(file)}:${symbolName}` : `:${symbolName}`;
|
|
247
|
+
return this.index.callGraph.filter((edge) => file ? edge.callee === suffix : edge.callee.endsWith(suffix));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get all call edges originating from the given symbol.
|
|
251
|
+
*
|
|
252
|
+
* @param symbolName - Name of the calling function/method
|
|
253
|
+
* @param file - Optional file path to narrow the search
|
|
254
|
+
* @returns Call edges originating from this symbol
|
|
255
|
+
*/
|
|
256
|
+
getCalleesOf(symbolName, file) {
|
|
257
|
+
const suffix = file ? `${resolve(file)}:${symbolName}` : `:${symbolName}`;
|
|
258
|
+
return this.index.callGraph.filter((edge) => file ? edge.caller === suffix : edge.caller.endsWith(suffix));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Calculate the blast radius of changing a file — which files are affected,
|
|
262
|
+
* which tests may break, and what's the risk level.
|
|
263
|
+
*
|
|
264
|
+
* @param file - Absolute file path being changed
|
|
265
|
+
* @returns Blast radius analysis
|
|
266
|
+
*/
|
|
267
|
+
getBlastRadius(file) {
|
|
268
|
+
const absFile = resolve(file);
|
|
269
|
+
const analysis = this.index.files.get(absFile);
|
|
270
|
+
// Direct dependents
|
|
271
|
+
const directDeps = this.reverseDepMap.get(absFile);
|
|
272
|
+
const directDependents = directDeps ? [...directDeps] : [];
|
|
273
|
+
// Transitive dependents (BFS)
|
|
274
|
+
const transitiveDependents = new Set();
|
|
275
|
+
const visited = new Set([absFile]);
|
|
276
|
+
const queue = [...directDependents];
|
|
277
|
+
for (const d of directDependents) {
|
|
278
|
+
visited.add(d);
|
|
279
|
+
transitiveDependents.add(d);
|
|
280
|
+
}
|
|
281
|
+
while (queue.length > 0) {
|
|
282
|
+
const current = queue.shift();
|
|
283
|
+
const deps = this.reverseDepMap.get(current);
|
|
284
|
+
if (!deps)
|
|
285
|
+
continue;
|
|
286
|
+
for (const dep of deps) {
|
|
287
|
+
if (!visited.has(dep)) {
|
|
288
|
+
visited.add(dep);
|
|
289
|
+
transitiveDependents.add(dep);
|
|
290
|
+
queue.push(dep);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Filter test files
|
|
295
|
+
const allAffected = [...transitiveDependents];
|
|
296
|
+
const affectedTests = allAffected.filter((f) => TEST_FILE_PATTERNS.some((p) => p.test(f)));
|
|
297
|
+
// Affected exports
|
|
298
|
+
const affectedExports = analysis
|
|
299
|
+
? analysis.symbols.filter((s) => s.exported).map((s) => s.name)
|
|
300
|
+
: [];
|
|
301
|
+
// Risk level
|
|
302
|
+
const totalAffected = transitiveDependents.size;
|
|
303
|
+
let riskLevel;
|
|
304
|
+
if (totalAffected <= 3 && affectedExports.length <= 2) {
|
|
305
|
+
riskLevel = "low";
|
|
306
|
+
}
|
|
307
|
+
else if (totalAffected <= 10 || affectedExports.length <= 5) {
|
|
308
|
+
riskLevel = "medium";
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
riskLevel = "high";
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
directDependents,
|
|
315
|
+
transitiveDependents: allAffected,
|
|
316
|
+
affectedTests,
|
|
317
|
+
affectedExports,
|
|
318
|
+
riskLevel,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// ─── Search ───
|
|
322
|
+
/**
|
|
323
|
+
* Search symbols by a query string using token-based fuzzy matching.
|
|
324
|
+
* Splits the query into tokens and scores symbols by match count against
|
|
325
|
+
* name, kind, file path, and JSDoc.
|
|
326
|
+
*
|
|
327
|
+
* @param query - Search query (space-separated tokens)
|
|
328
|
+
* @param limit - Maximum results to return (default 10)
|
|
329
|
+
* @returns Ranked search results with relevance scores
|
|
330
|
+
*/
|
|
331
|
+
searchSymbols(query, limit = 10) {
|
|
332
|
+
const tokens = query
|
|
333
|
+
.toLowerCase()
|
|
334
|
+
.split(/[\s_\-./]+/)
|
|
335
|
+
.filter((t) => t.length > 0);
|
|
336
|
+
if (tokens.length === 0)
|
|
337
|
+
return [];
|
|
338
|
+
const scored = [];
|
|
339
|
+
for (const [name, symbols] of this.index.symbolTable) {
|
|
340
|
+
for (const sym of symbols) {
|
|
341
|
+
const score = this.scoreSymbol(sym, name, tokens);
|
|
342
|
+
if (score > 0) {
|
|
343
|
+
scored.push({
|
|
344
|
+
symbol: sym,
|
|
345
|
+
file: sym.file,
|
|
346
|
+
relevance: score,
|
|
347
|
+
snippet: this.getSnippet(sym),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Sort by relevance descending, then by name
|
|
353
|
+
scored.sort((a, b) => b.relevance - a.relevance || a.symbol.name.localeCompare(b.symbol.name));
|
|
354
|
+
return scored.slice(0, limit);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Search for symbols whose return type or type annotation matches a pattern.
|
|
358
|
+
*
|
|
359
|
+
* @param typePattern - Regex pattern to match against types
|
|
360
|
+
* @returns Matching symbols
|
|
361
|
+
*/
|
|
362
|
+
searchByType(typePattern) {
|
|
363
|
+
const re = new RegExp(typePattern, "i");
|
|
364
|
+
const results = [];
|
|
365
|
+
for (const symbols of this.index.symbolTable.values()) {
|
|
366
|
+
for (const sym of symbols) {
|
|
367
|
+
if (sym.returnType && re.test(sym.returnType)) {
|
|
368
|
+
results.push(sym);
|
|
369
|
+
}
|
|
370
|
+
if (sym.params) {
|
|
371
|
+
for (const param of sym.params) {
|
|
372
|
+
if (re.test(param.type)) {
|
|
373
|
+
results.push(sym);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
// ─── File analysis ───
|
|
383
|
+
/**
|
|
384
|
+
* Get the analysis for a specific file.
|
|
385
|
+
*
|
|
386
|
+
* @param file - Absolute file path
|
|
387
|
+
* @returns FileAnalysis or undefined if not indexed
|
|
388
|
+
*/
|
|
389
|
+
getFileAnalysis(file) {
|
|
390
|
+
return this.index.files.get(resolve(file));
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get files exceeding a cyclomatic complexity threshold.
|
|
394
|
+
*
|
|
395
|
+
* @param threshold - Minimum cyclomatic complexity (default 20)
|
|
396
|
+
* @returns Files exceeding the threshold, sorted by complexity descending
|
|
397
|
+
*/
|
|
398
|
+
getComplexFiles(threshold = 20) {
|
|
399
|
+
const results = [];
|
|
400
|
+
for (const analysis of this.index.files.values()) {
|
|
401
|
+
if (analysis.complexity.cyclomatic >= threshold) {
|
|
402
|
+
results.push(analysis);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
results.sort((a, b) => b.complexity.cyclomatic - a.complexity.cyclomatic);
|
|
406
|
+
return results;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Identify hotspots — files with high complexity AND many dependents.
|
|
410
|
+
*
|
|
411
|
+
* @returns Hotspot entries sorted by combined score descending
|
|
412
|
+
*/
|
|
413
|
+
getHotspots() {
|
|
414
|
+
const hotspots = [];
|
|
415
|
+
for (const [file, analysis] of this.index.files) {
|
|
416
|
+
const deps = this.reverseDepMap.get(file);
|
|
417
|
+
const depCount = deps ? deps.size : 0;
|
|
418
|
+
hotspots.push({
|
|
419
|
+
file,
|
|
420
|
+
complexity: analysis.complexity.cyclomatic,
|
|
421
|
+
dependencies: depCount,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// Sort by combined score (complexity * log(deps+1))
|
|
425
|
+
hotspots.sort((a, b) => {
|
|
426
|
+
const scoreA = a.complexity * Math.log2(a.dependencies + 1);
|
|
427
|
+
const scoreB = b.complexity * Math.log2(b.dependencies + 1);
|
|
428
|
+
return scoreB - scoreA;
|
|
429
|
+
});
|
|
430
|
+
return hotspots;
|
|
431
|
+
}
|
|
432
|
+
// ─── Stats ───
|
|
433
|
+
/**
|
|
434
|
+
* Get summary statistics for the indexed codebase.
|
|
435
|
+
*
|
|
436
|
+
* @returns Aggregate stats
|
|
437
|
+
*/
|
|
438
|
+
getStats() {
|
|
439
|
+
let totalComplexity = 0;
|
|
440
|
+
for (const analysis of this.index.files.values()) {
|
|
441
|
+
totalComplexity += analysis.complexity.cyclomatic;
|
|
442
|
+
}
|
|
443
|
+
const avgComplexity = this.index.totalFiles > 0
|
|
444
|
+
? totalComplexity / this.index.totalFiles
|
|
445
|
+
: 0;
|
|
446
|
+
return {
|
|
447
|
+
totalFiles: this.index.totalFiles,
|
|
448
|
+
totalSymbols: this.index.totalSymbols,
|
|
449
|
+
avgComplexity: Math.round(avgComplexity * 100) / 100,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
// ─── Private: File collection ───
|
|
453
|
+
/**
|
|
454
|
+
* Recursively collect all source files under a directory,
|
|
455
|
+
* skipping excluded directories.
|
|
456
|
+
*/
|
|
457
|
+
async collectSourceFiles(dir) {
|
|
458
|
+
const results = [];
|
|
459
|
+
let entries;
|
|
460
|
+
try {
|
|
461
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return results;
|
|
465
|
+
}
|
|
466
|
+
for (const entry of entries) {
|
|
467
|
+
const fullPath = join(dir, entry.name);
|
|
468
|
+
if (entry.isDirectory()) {
|
|
469
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
470
|
+
const sub = await this.collectSourceFiles(fullPath);
|
|
471
|
+
results.push(...sub);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (entry.isFile() && this.isSupportedSourceFile(entry.name)) {
|
|
475
|
+
results.push(fullPath);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return results;
|
|
479
|
+
}
|
|
480
|
+
// ─── Private: File support check ───
|
|
481
|
+
/**
|
|
482
|
+
* Check if a file is a supported source file.
|
|
483
|
+
* Uses LanguageSupport for multi-language detection when available,
|
|
484
|
+
* otherwise falls back to built-in TS/JS extensions.
|
|
485
|
+
*/
|
|
486
|
+
isSupportedSourceFile(fileName) {
|
|
487
|
+
const ext = extname(fileName);
|
|
488
|
+
if (SOURCE_EXTENSIONS.has(ext))
|
|
489
|
+
return true;
|
|
490
|
+
if (this.languageSupport) {
|
|
491
|
+
const lang = this.languageSupport.detectLanguage(fileName);
|
|
492
|
+
return lang !== "unknown";
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
// ─── Private: File analysis ───
|
|
497
|
+
/**
|
|
498
|
+
* Analyze a single source file — extract symbols, imports, exports,
|
|
499
|
+
* and compute complexity metrics.
|
|
500
|
+
*
|
|
501
|
+
* When LanguageSupport is available and the file is not TS/JS,
|
|
502
|
+
* uses language-specific patterns for symbol extraction.
|
|
503
|
+
*/
|
|
504
|
+
analyzeFile(filePath, content) {
|
|
505
|
+
const ext = extname(filePath);
|
|
506
|
+
const isTsJs = ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx";
|
|
507
|
+
// Detect language — use LanguageSupport when available, fallback to TS/JS
|
|
508
|
+
let language;
|
|
509
|
+
if (isTsJs) {
|
|
510
|
+
language = ext === ".ts" || ext === ".tsx" ? "typescript" : "javascript";
|
|
511
|
+
}
|
|
512
|
+
else if (this.languageSupport) {
|
|
513
|
+
language = this.languageSupport.detectLanguage(filePath, content);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
language = "javascript"; // shouldn't happen, but safe fallback
|
|
517
|
+
}
|
|
518
|
+
const lines = content.split("\n");
|
|
519
|
+
// For TS/JS files or when LanguageSupport is unavailable, use built-in extraction
|
|
520
|
+
// For other languages, use LanguageSupport patterns for basic symbol extraction
|
|
521
|
+
let symbols;
|
|
522
|
+
if (isTsJs || !this.languageSupport) {
|
|
523
|
+
symbols = this.extractSymbols(filePath, content, lines);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
symbols = this.extractSymbolsWithLanguageSupport(filePath, content, lines, language);
|
|
527
|
+
}
|
|
528
|
+
const imports = this.extractImports(content);
|
|
529
|
+
const exports = this.extractExports(content);
|
|
530
|
+
const callEdges = this.extractCallEdges(filePath, content, lines, symbols);
|
|
531
|
+
const complexity = this.computeComplexity(content, lines, imports.length);
|
|
532
|
+
return { file: filePath, language, symbols, imports, exports, callEdges, complexity };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Extract all symbol definitions from file content using regex patterns.
|
|
536
|
+
*/
|
|
537
|
+
extractSymbols(filePath, content, lines) {
|
|
538
|
+
const symbols = [];
|
|
539
|
+
const totalLines = lines.length;
|
|
540
|
+
// ── Functions ──
|
|
541
|
+
const funcRe = new RegExp(FUNCTION_RE.source, "gm");
|
|
542
|
+
let match;
|
|
543
|
+
while ((match = funcRe.exec(content)) !== null) {
|
|
544
|
+
const jsdoc = match[1]?.trim();
|
|
545
|
+
const exportKeyword = match[2]?.trim() ?? "";
|
|
546
|
+
const asyncKeyword = match[3]?.trim() ?? "";
|
|
547
|
+
const name = match[4];
|
|
548
|
+
const rawParams = match[5] ?? "";
|
|
549
|
+
const returnType = match[6]?.trim();
|
|
550
|
+
const line = this.getLineNumber(content, match.index);
|
|
551
|
+
symbols.push({
|
|
552
|
+
name,
|
|
553
|
+
kind: "function",
|
|
554
|
+
file: filePath,
|
|
555
|
+
line,
|
|
556
|
+
endLine: this.estimateEndLine(lines, line, totalLines),
|
|
557
|
+
params: this.parseParams(rawParams),
|
|
558
|
+
returnType: returnType || undefined,
|
|
559
|
+
exported: exportKeyword.length > 0,
|
|
560
|
+
isDefault: exportKeyword.includes("default"),
|
|
561
|
+
isAsync: asyncKeyword === "async",
|
|
562
|
+
jsdoc: jsdoc && jsdoc.startsWith("/**") ? this.cleanJsdoc(jsdoc) : undefined,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
// ── Arrow functions ──
|
|
566
|
+
const arrowRe = new RegExp(ARROW_FUNCTION_RE.source, "gm");
|
|
567
|
+
while ((match = arrowRe.exec(content)) !== null) {
|
|
568
|
+
const jsdoc = match[1]?.trim();
|
|
569
|
+
const exportKeyword = match[2]?.trim() ?? "";
|
|
570
|
+
const name = match[4];
|
|
571
|
+
const asyncKeyword = match[5]?.trim() ?? "";
|
|
572
|
+
const line = this.getLineNumber(content, match.index);
|
|
573
|
+
symbols.push({
|
|
574
|
+
name,
|
|
575
|
+
kind: "function",
|
|
576
|
+
file: filePath,
|
|
577
|
+
line,
|
|
578
|
+
endLine: this.estimateEndLine(lines, line, totalLines),
|
|
579
|
+
exported: exportKeyword.length > 0,
|
|
580
|
+
isDefault: exportKeyword.includes("default"),
|
|
581
|
+
isAsync: asyncKeyword === "async",
|
|
582
|
+
jsdoc: jsdoc && jsdoc.startsWith("/**") ? this.cleanJsdoc(jsdoc) : undefined,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
// ── Classes ──
|
|
586
|
+
const classRe = new RegExp(CLASS_RE.source, "gm");
|
|
587
|
+
while ((match = classRe.exec(content)) !== null) {
|
|
588
|
+
const jsdoc = match[1]?.trim();
|
|
589
|
+
const exportKeyword = match[2]?.trim() ?? "";
|
|
590
|
+
const name = match[4];
|
|
591
|
+
const extendsName = match[5]?.replace(/<[^>]*>/, "").trim();
|
|
592
|
+
const implementsRaw = match[6]?.trim();
|
|
593
|
+
const line = this.getLineNumber(content, match.index);
|
|
594
|
+
const endLine = this.estimateBlockEnd(lines, line, totalLines);
|
|
595
|
+
// Extract class members
|
|
596
|
+
const classBody = lines.slice(line - 1, endLine).join("\n");
|
|
597
|
+
const members = this.extractClassMembers(filePath, classBody, line);
|
|
598
|
+
symbols.push({
|
|
599
|
+
name,
|
|
600
|
+
kind: "class",
|
|
601
|
+
file: filePath,
|
|
602
|
+
line,
|
|
603
|
+
endLine,
|
|
604
|
+
extends: extendsName || undefined,
|
|
605
|
+
implements: implementsRaw
|
|
606
|
+
? implementsRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
607
|
+
: undefined,
|
|
608
|
+
members: members.length > 0 ? members : undefined,
|
|
609
|
+
exported: exportKeyword.length > 0,
|
|
610
|
+
isDefault: exportKeyword.includes("default"),
|
|
611
|
+
isAsync: false,
|
|
612
|
+
jsdoc: jsdoc && jsdoc.startsWith("/**") ? this.cleanJsdoc(jsdoc) : undefined,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// ── Interfaces ──
|
|
616
|
+
const ifaceRe = new RegExp(INTERFACE_RE.source, "gm");
|
|
617
|
+
while ((match = ifaceRe.exec(content)) !== null) {
|
|
618
|
+
const jsdoc = match[1]?.trim();
|
|
619
|
+
const exportKeyword = match[2]?.trim() ?? "";
|
|
620
|
+
const name = match[3];
|
|
621
|
+
const line = this.getLineNumber(content, match.index);
|
|
622
|
+
symbols.push({
|
|
623
|
+
name,
|
|
624
|
+
kind: "interface",
|
|
625
|
+
file: filePath,
|
|
626
|
+
line,
|
|
627
|
+
endLine: this.estimateBlockEnd(lines, line, totalLines),
|
|
628
|
+
exported: exportKeyword.length > 0,
|
|
629
|
+
isDefault: exportKeyword.includes("default"),
|
|
630
|
+
isAsync: false,
|
|
631
|
+
jsdoc: jsdoc && jsdoc.startsWith("/**") ? this.cleanJsdoc(jsdoc) : undefined,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
// ── Type aliases ──
|
|
635
|
+
const typeRe = new RegExp(TYPE_ALIAS_RE.source, "gm");
|
|
636
|
+
while ((match = typeRe.exec(content)) !== null) {
|
|
637
|
+
const jsdoc = match[1]?.trim();
|
|
638
|
+
const exportKeyword = match[2]?.trim() ?? "";
|
|
639
|
+
const name = match[3];
|
|
640
|
+
const line = this.getLineNumber(content, match.index);
|
|
641
|
+
symbols.push({
|
|
642
|
+
name,
|
|
643
|
+
kind: "type",
|
|
644
|
+
file: filePath,
|
|
645
|
+
line,
|
|
646
|
+
endLine: this.estimateTypeEnd(lines, line, totalLines),
|
|
647
|
+
exported: exportKeyword.length > 0,
|
|
648
|
+
isDefault: exportKeyword.includes("default"),
|
|
649
|
+
isAsync: false,
|
|
650
|
+
jsdoc: jsdoc && jsdoc.startsWith("/**") ? this.cleanJsdoc(jsdoc) : undefined,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// ── Enums ──
|
|
654
|
+
const enumRe = new RegExp(ENUM_RE.source, "gm");
|
|
655
|
+
while ((match = enumRe.exec(content)) !== null) {
|
|
656
|
+
const jsdoc = match[1]?.trim();
|
|
657
|
+
const exportKeyword = match[2]?.trim() ?? "";
|
|
658
|
+
const name = match[4];
|
|
659
|
+
const line = this.getLineNumber(content, match.index);
|
|
660
|
+
symbols.push({
|
|
661
|
+
name,
|
|
662
|
+
kind: "enum",
|
|
663
|
+
file: filePath,
|
|
664
|
+
line,
|
|
665
|
+
endLine: this.estimateBlockEnd(lines, line, totalLines),
|
|
666
|
+
exported: exportKeyword.length > 0,
|
|
667
|
+
isDefault: exportKeyword.includes("default"),
|
|
668
|
+
isAsync: false,
|
|
669
|
+
jsdoc: jsdoc && jsdoc.startsWith("/**") ? this.cleanJsdoc(jsdoc) : undefined,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
return symbols;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Extract symbols from non-TS/JS files using LanguageSupport patterns.
|
|
676
|
+
* Provides basic function and class extraction for any language
|
|
677
|
+
* that LanguageSupport has patterns for.
|
|
678
|
+
*
|
|
679
|
+
* Falls back to an empty array if patterns produce no matches — this is
|
|
680
|
+
* safe because the caller only reaches here for non-TS/JS files.
|
|
681
|
+
*/
|
|
682
|
+
extractSymbolsWithLanguageSupport(filePath, content, lines, language) {
|
|
683
|
+
if (!this.languageSupport)
|
|
684
|
+
return [];
|
|
685
|
+
const symbols = [];
|
|
686
|
+
const totalLines = lines.length;
|
|
687
|
+
// Extract functions via LanguageSupport patterns
|
|
688
|
+
const functions = this.languageSupport.extractFunctions(content, language);
|
|
689
|
+
for (const fn of functions) {
|
|
690
|
+
symbols.push({
|
|
691
|
+
name: fn.name,
|
|
692
|
+
kind: "function",
|
|
693
|
+
file: filePath,
|
|
694
|
+
line: fn.line,
|
|
695
|
+
endLine: this.estimateEndLine(lines, fn.line, totalLines),
|
|
696
|
+
params: fn.params ? this.parseParams(fn.params) : undefined,
|
|
697
|
+
returnType: fn.returnType || undefined,
|
|
698
|
+
exported: fn.visibility === "public" || fn.visibility === undefined,
|
|
699
|
+
isDefault: false,
|
|
700
|
+
isAsync: false,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
// Extract classes via LanguageSupport patterns
|
|
704
|
+
const classes = this.languageSupport.extractClasses(content, language);
|
|
705
|
+
for (const cls of classes) {
|
|
706
|
+
symbols.push({
|
|
707
|
+
name: cls.name,
|
|
708
|
+
kind: "class",
|
|
709
|
+
file: filePath,
|
|
710
|
+
line: cls.line,
|
|
711
|
+
endLine: this.estimateBlockEnd(lines, cls.line, totalLines),
|
|
712
|
+
exported: cls.visibility === "public" || cls.visibility === undefined,
|
|
713
|
+
isDefault: false,
|
|
714
|
+
isAsync: false,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
// Extract interfaces if the language supports them
|
|
718
|
+
const patterns = this.languageSupport.getPatterns(language);
|
|
719
|
+
if (patterns.interface) {
|
|
720
|
+
const ifaceRe = new RegExp(patterns.interface.source, patterns.interface.flags);
|
|
721
|
+
let match;
|
|
722
|
+
while ((match = ifaceRe.exec(content)) !== null) {
|
|
723
|
+
const name = match[1];
|
|
724
|
+
if (!name)
|
|
725
|
+
continue;
|
|
726
|
+
const line = this.getLineNumber(content, match.index);
|
|
727
|
+
symbols.push({
|
|
728
|
+
name,
|
|
729
|
+
kind: "interface",
|
|
730
|
+
file: filePath,
|
|
731
|
+
line,
|
|
732
|
+
endLine: this.estimateBlockEnd(lines, line, totalLines),
|
|
733
|
+
exported: true,
|
|
734
|
+
isDefault: false,
|
|
735
|
+
isAsync: false,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return symbols;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Extract class method members from a class body.
|
|
743
|
+
*/
|
|
744
|
+
extractClassMembers(filePath, classBody, classStartLine) {
|
|
745
|
+
const members = [];
|
|
746
|
+
const methodRe = new RegExp(METHOD_RE.source, "gm");
|
|
747
|
+
let match;
|
|
748
|
+
while ((match = methodRe.exec(classBody)) !== null) {
|
|
749
|
+
const name = match[5];
|
|
750
|
+
// Skip constructor-like patterns and braces
|
|
751
|
+
if (!name || name === "constructor" && false) {
|
|
752
|
+
// include constructor
|
|
753
|
+
}
|
|
754
|
+
if (!name || /^[{}]$/.test(name))
|
|
755
|
+
continue;
|
|
756
|
+
const asyncKeyword = match[3]?.trim() ?? "";
|
|
757
|
+
const rawParams = match[6] ?? "";
|
|
758
|
+
const returnType = match[7]?.trim();
|
|
759
|
+
const localLine = this.getLineNumber(classBody, match.index);
|
|
760
|
+
const absoluteLine = classStartLine + localLine - 1;
|
|
761
|
+
members.push({
|
|
762
|
+
name,
|
|
763
|
+
kind: "method",
|
|
764
|
+
file: filePath,
|
|
765
|
+
line: absoluteLine,
|
|
766
|
+
endLine: absoluteLine + 5, // rough estimate for methods
|
|
767
|
+
params: this.parseParams(rawParams),
|
|
768
|
+
returnType: returnType || undefined,
|
|
769
|
+
exported: false,
|
|
770
|
+
isDefault: false,
|
|
771
|
+
isAsync: asyncKeyword === "async",
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return members;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Extract import references from file content.
|
|
778
|
+
*/
|
|
779
|
+
extractImports(content) {
|
|
780
|
+
const refs = [];
|
|
781
|
+
let match;
|
|
782
|
+
// Standard imports
|
|
783
|
+
const importRe = new RegExp(IMPORT_RE.source, "g");
|
|
784
|
+
while ((match = importRe.exec(content)) !== null) {
|
|
785
|
+
const namedGroup = match[1];
|
|
786
|
+
const defaultImport = match[2];
|
|
787
|
+
const namespaceImport = match[3];
|
|
788
|
+
const source = match[4];
|
|
789
|
+
const symbols = [];
|
|
790
|
+
if (namedGroup) {
|
|
791
|
+
symbols.push(...namedGroup.split(",").map((s) => s.trim().split(/\s+as\s+/)[0]).filter(Boolean));
|
|
792
|
+
}
|
|
793
|
+
if (defaultImport)
|
|
794
|
+
symbols.push(defaultImport);
|
|
795
|
+
if (namespaceImport)
|
|
796
|
+
symbols.push(namespaceImport);
|
|
797
|
+
const isTypeOnly = /import\s+type\s+/.test(match[0]);
|
|
798
|
+
refs.push({ source, symbols, isTypeOnly });
|
|
799
|
+
}
|
|
800
|
+
// Re-exports
|
|
801
|
+
const reExportRe = new RegExp(RE_EXPORT_RE.source, "g");
|
|
802
|
+
while ((match = reExportRe.exec(content)) !== null) {
|
|
803
|
+
const namedGroup = match[1];
|
|
804
|
+
const source = match[2];
|
|
805
|
+
const symbols = namedGroup
|
|
806
|
+
.split(",")
|
|
807
|
+
.map((s) => s.trim().split(/\s+as\s+/)[0])
|
|
808
|
+
.filter(Boolean);
|
|
809
|
+
const isTypeOnly = /export\s+type\s+\{/.test(match[0]);
|
|
810
|
+
refs.push({ source, symbols, isTypeOnly });
|
|
811
|
+
}
|
|
812
|
+
return refs;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Extract exported symbol names from file content.
|
|
816
|
+
*/
|
|
817
|
+
extractExports(content) {
|
|
818
|
+
const exports = [];
|
|
819
|
+
let match;
|
|
820
|
+
const namedRe = new RegExp(EXPORT_NAMED_RE.source, "g");
|
|
821
|
+
while ((match = namedRe.exec(content)) !== null) {
|
|
822
|
+
exports.push(match[1]);
|
|
823
|
+
}
|
|
824
|
+
const defaultRe = new RegExp(EXPORT_DEFAULT_RE.source, "g");
|
|
825
|
+
if (defaultRe.exec(content) !== null) {
|
|
826
|
+
exports.push("default");
|
|
827
|
+
}
|
|
828
|
+
const listRe = new RegExp(EXPORT_LIST_RE.source, "g");
|
|
829
|
+
while ((match = listRe.exec(content)) !== null) {
|
|
830
|
+
const symbols = match[1]
|
|
831
|
+
.split(",")
|
|
832
|
+
.map((s) => {
|
|
833
|
+
const parts = s.trim().split(/\s+as\s+/);
|
|
834
|
+
return parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
835
|
+
})
|
|
836
|
+
.filter(Boolean);
|
|
837
|
+
exports.push(...symbols);
|
|
838
|
+
}
|
|
839
|
+
return [...new Set(exports)];
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Extract call edges from function bodies.
|
|
843
|
+
* Maps each function's body to the functions it calls.
|
|
844
|
+
*/
|
|
845
|
+
extractCallEdges(filePath, content, lines, symbols) {
|
|
846
|
+
const edges = [];
|
|
847
|
+
// For each function/method symbol, scan its body for call sites
|
|
848
|
+
for (const sym of symbols) {
|
|
849
|
+
if (sym.kind !== "function" && sym.kind !== "method")
|
|
850
|
+
continue;
|
|
851
|
+
const bodyStart = Math.max(0, sym.line - 1);
|
|
852
|
+
const bodyEnd = Math.min(lines.length, sym.endLine);
|
|
853
|
+
const body = lines.slice(bodyStart, bodyEnd).join("\n");
|
|
854
|
+
const callRe = new RegExp(CALL_SITE_RE.source, "g");
|
|
855
|
+
let callMatch;
|
|
856
|
+
while ((callMatch = callRe.exec(body)) !== null) {
|
|
857
|
+
const calleeName = callMatch[1];
|
|
858
|
+
if (BUILTIN_CALLS.has(calleeName))
|
|
859
|
+
continue;
|
|
860
|
+
if (calleeName === sym.name)
|
|
861
|
+
continue; // skip self-recursion noise
|
|
862
|
+
const callLine = bodyStart + this.getLineNumber(body, callMatch.index);
|
|
863
|
+
edges.push({
|
|
864
|
+
caller: `${filePath}:${sym.name}`,
|
|
865
|
+
callee: `${filePath}:${calleeName}`,
|
|
866
|
+
line: callLine,
|
|
867
|
+
file: filePath,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return edges;
|
|
872
|
+
}
|
|
873
|
+
// ─── Private: Complexity computation ───
|
|
874
|
+
/**
|
|
875
|
+
* Compute complexity metrics for file content.
|
|
876
|
+
*/
|
|
877
|
+
computeComplexity(content, lines, importCount) {
|
|
878
|
+
let blankLines = 0;
|
|
879
|
+
let commentLines = 0;
|
|
880
|
+
let inBlockComment = false;
|
|
881
|
+
for (const line of lines) {
|
|
882
|
+
const trimmed = line.trim();
|
|
883
|
+
if (trimmed === "") {
|
|
884
|
+
blankLines++;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (inBlockComment) {
|
|
888
|
+
commentLines++;
|
|
889
|
+
if (trimmed.includes("*/"))
|
|
890
|
+
inBlockComment = false;
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (trimmed.startsWith("//")) {
|
|
894
|
+
commentLines++;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (trimmed.startsWith("/*")) {
|
|
898
|
+
commentLines++;
|
|
899
|
+
inBlockComment = !trimmed.includes("*/");
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const loc = lines.length - blankLines - commentLines;
|
|
904
|
+
// Cyclomatic complexity: count branching constructs
|
|
905
|
+
const cyclomaticPatterns = /\b(?:if|else\s+if|for|while|do|switch|case|catch)\b|\?\?|&&|\|\||\?(?=[^?:])/g;
|
|
906
|
+
const cyclomaticMatches = content.match(cyclomaticPatterns);
|
|
907
|
+
const cyclomatic = (cyclomaticMatches?.length ?? 0) + 1; // base complexity = 1
|
|
908
|
+
// Cognitive complexity: nesting-aware scoring
|
|
909
|
+
const cognitive = this.computeCognitiveComplexity(lines);
|
|
910
|
+
return {
|
|
911
|
+
cyclomatic,
|
|
912
|
+
cognitive,
|
|
913
|
+
loc,
|
|
914
|
+
blankLines,
|
|
915
|
+
commentLines,
|
|
916
|
+
dependencies: importCount,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Compute cognitive complexity — increments with nesting depth.
|
|
921
|
+
*/
|
|
922
|
+
computeCognitiveComplexity(lines) {
|
|
923
|
+
let complexity = 0;
|
|
924
|
+
let nesting = 0;
|
|
925
|
+
const branchRe = /\b(if|else\s+if|for|while|do|switch|catch)\b/;
|
|
926
|
+
const nestingIncrease = /\{/g;
|
|
927
|
+
const nestingDecrease = /\}/g;
|
|
928
|
+
for (const line of lines) {
|
|
929
|
+
const trimmed = line.trim();
|
|
930
|
+
if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (branchRe.test(trimmed)) {
|
|
934
|
+
// +1 for the construct, + nesting level for being nested
|
|
935
|
+
complexity += 1 + nesting;
|
|
936
|
+
}
|
|
937
|
+
// Track nesting
|
|
938
|
+
const opens = trimmed.match(nestingIncrease);
|
|
939
|
+
const closes = trimmed.match(nestingDecrease);
|
|
940
|
+
nesting += (opens?.length ?? 0) - (closes?.length ?? 0);
|
|
941
|
+
if (nesting < 0)
|
|
942
|
+
nesting = 0;
|
|
943
|
+
}
|
|
944
|
+
return complexity;
|
|
945
|
+
}
|
|
946
|
+
// ─── Private: Index builders ───
|
|
947
|
+
/**
|
|
948
|
+
* Build the symbol table from all file analyses.
|
|
949
|
+
* Maps symbol name → all SymbolInfo across the codebase.
|
|
950
|
+
*/
|
|
951
|
+
buildSymbolTable() {
|
|
952
|
+
this.index.symbolTable.clear();
|
|
953
|
+
for (const analysis of this.index.files.values()) {
|
|
954
|
+
for (const sym of analysis.symbols) {
|
|
955
|
+
let list = this.index.symbolTable.get(sym.name);
|
|
956
|
+
if (!list) {
|
|
957
|
+
list = [];
|
|
958
|
+
this.index.symbolTable.set(sym.name, list);
|
|
959
|
+
}
|
|
960
|
+
list.push(sym);
|
|
961
|
+
// Also index class/interface members
|
|
962
|
+
if (sym.members) {
|
|
963
|
+
for (const member of sym.members) {
|
|
964
|
+
let memberList = this.index.symbolTable.get(member.name);
|
|
965
|
+
if (!memberList) {
|
|
966
|
+
memberList = [];
|
|
967
|
+
this.index.symbolTable.set(member.name, memberList);
|
|
968
|
+
}
|
|
969
|
+
memberList.push(member);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Build the global call graph from per-file call edges.
|
|
977
|
+
*/
|
|
978
|
+
buildCallGraph() {
|
|
979
|
+
this.index.callGraph = [];
|
|
980
|
+
for (const analysis of this.index.files.values()) {
|
|
981
|
+
this.index.callGraph.push(...analysis.callEdges);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Build the reverse dependency map (file → files that import it).
|
|
986
|
+
* Resolves relative import paths to absolute file paths.
|
|
987
|
+
*/
|
|
988
|
+
buildReverseDependencyMap() {
|
|
989
|
+
this.reverseDepMap.clear();
|
|
990
|
+
const knownFiles = [...this.index.files.keys()];
|
|
991
|
+
for (const [filePath, analysis] of this.index.files) {
|
|
992
|
+
for (const imp of analysis.imports) {
|
|
993
|
+
if (!imp.source.startsWith("."))
|
|
994
|
+
continue;
|
|
995
|
+
const resolved = this.resolveImportPath(filePath, imp.source, knownFiles);
|
|
996
|
+
if (resolved) {
|
|
997
|
+
let deps = this.reverseDepMap.get(resolved);
|
|
998
|
+
if (!deps) {
|
|
999
|
+
deps = new Set();
|
|
1000
|
+
this.reverseDepMap.set(resolved, deps);
|
|
1001
|
+
}
|
|
1002
|
+
deps.add(filePath);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// ─── Private: Utility helpers ───
|
|
1008
|
+
/**
|
|
1009
|
+
* Resolve a relative import specifier to an absolute file path.
|
|
1010
|
+
*/
|
|
1011
|
+
resolveImportPath(fromFile, importPath, knownFiles) {
|
|
1012
|
+
const dir = resolve(fromFile, "..");
|
|
1013
|
+
let resolved = resolve(dir, importPath);
|
|
1014
|
+
if (knownFiles.includes(resolved))
|
|
1015
|
+
return resolved;
|
|
1016
|
+
// .js → .ts mapping (common in ESM TypeScript)
|
|
1017
|
+
if (resolved.endsWith(".js")) {
|
|
1018
|
+
const tsPath = resolved.slice(0, -3) + ".ts";
|
|
1019
|
+
if (knownFiles.includes(tsPath))
|
|
1020
|
+
return tsPath;
|
|
1021
|
+
const tsxPath = resolved.slice(0, -3) + ".tsx";
|
|
1022
|
+
if (knownFiles.includes(tsxPath))
|
|
1023
|
+
return tsxPath;
|
|
1024
|
+
}
|
|
1025
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
1026
|
+
const withExt = resolved + ext;
|
|
1027
|
+
if (knownFiles.includes(withExt))
|
|
1028
|
+
return withExt;
|
|
1029
|
+
}
|
|
1030
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
1031
|
+
const indexPath = join(resolved, `index${ext}`);
|
|
1032
|
+
if (knownFiles.includes(indexPath))
|
|
1033
|
+
return indexPath;
|
|
1034
|
+
}
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get 1-based line number for a character offset within content.
|
|
1039
|
+
*/
|
|
1040
|
+
getLineNumber(content, offset) {
|
|
1041
|
+
let line = 1;
|
|
1042
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
1043
|
+
if (content[i] === "\n")
|
|
1044
|
+
line++;
|
|
1045
|
+
}
|
|
1046
|
+
return line;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Estimate the end line of a function/arrow function by scanning for matching braces.
|
|
1050
|
+
*/
|
|
1051
|
+
estimateEndLine(lines, startLine, totalLines) {
|
|
1052
|
+
return this.estimateBlockEnd(lines, startLine, totalLines);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Estimate the end line of a brace-delimited block (class, function, enum, interface).
|
|
1056
|
+
* Tracks `{` and `}` to find the matching close brace.
|
|
1057
|
+
*/
|
|
1058
|
+
estimateBlockEnd(lines, startLine, totalLines) {
|
|
1059
|
+
let depth = 0;
|
|
1060
|
+
let foundOpen = false;
|
|
1061
|
+
for (let i = startLine - 1; i < totalLines; i++) {
|
|
1062
|
+
const line = lines[i];
|
|
1063
|
+
for (const ch of line) {
|
|
1064
|
+
if (ch === "{") {
|
|
1065
|
+
depth++;
|
|
1066
|
+
foundOpen = true;
|
|
1067
|
+
}
|
|
1068
|
+
else if (ch === "}") {
|
|
1069
|
+
depth--;
|
|
1070
|
+
if (foundOpen && depth === 0) {
|
|
1071
|
+
return i + 1; // 1-based
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// Fallback: return startLine + reasonable range
|
|
1077
|
+
return Math.min(startLine + 20, totalLines);
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Estimate the end line of a type alias (ends with `;` at depth 0).
|
|
1081
|
+
*/
|
|
1082
|
+
estimateTypeEnd(lines, startLine, totalLines) {
|
|
1083
|
+
let depth = 0;
|
|
1084
|
+
for (let i = startLine - 1; i < totalLines; i++) {
|
|
1085
|
+
const line = lines[i];
|
|
1086
|
+
for (const ch of line) {
|
|
1087
|
+
if (ch === "{" || ch === "(")
|
|
1088
|
+
depth++;
|
|
1089
|
+
else if (ch === "}" || ch === ")")
|
|
1090
|
+
depth--;
|
|
1091
|
+
else if (ch === ";" && depth <= 0)
|
|
1092
|
+
return i + 1;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return Math.min(startLine + 10, totalLines);
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Parse a raw parameter string into ParamInfo array.
|
|
1099
|
+
* Handles `name: Type`, `name?: Type`, `name = defaultVal`.
|
|
1100
|
+
*/
|
|
1101
|
+
parseParams(raw) {
|
|
1102
|
+
if (!raw.trim())
|
|
1103
|
+
return [];
|
|
1104
|
+
const params = [];
|
|
1105
|
+
// Split on commas that are not inside angle brackets or parens
|
|
1106
|
+
const parts = this.splitParams(raw);
|
|
1107
|
+
for (const part of parts) {
|
|
1108
|
+
const trimmed = part.trim();
|
|
1109
|
+
if (!trimmed)
|
|
1110
|
+
continue;
|
|
1111
|
+
// Handle destructured params like { a, b }: Type
|
|
1112
|
+
const destructuredMatch = trimmed.match(/^(\{[^}]*\}|\[[^\]]*\])\s*(?::\s*(.+))?$/);
|
|
1113
|
+
if (destructuredMatch) {
|
|
1114
|
+
params.push({
|
|
1115
|
+
name: destructuredMatch[1],
|
|
1116
|
+
type: destructuredMatch[2]?.trim() ?? "unknown",
|
|
1117
|
+
optional: false,
|
|
1118
|
+
});
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
// Handle rest params: ...args: Type
|
|
1122
|
+
const restMatch = trimmed.match(/^\.\.\.(\w+)\s*(?::\s*(.+))?$/);
|
|
1123
|
+
if (restMatch) {
|
|
1124
|
+
params.push({
|
|
1125
|
+
name: `...${restMatch[1]}`,
|
|
1126
|
+
type: restMatch[2]?.trim() ?? "unknown[]",
|
|
1127
|
+
optional: false,
|
|
1128
|
+
});
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
// Standard: name?: Type = default
|
|
1132
|
+
const paramMatch = trimmed.match(/^(\w+)(\?)?\s*(?::\s*([^=]+))?\s*(?:=\s*(.+))?$/);
|
|
1133
|
+
if (paramMatch) {
|
|
1134
|
+
params.push({
|
|
1135
|
+
name: paramMatch[1],
|
|
1136
|
+
type: paramMatch[3]?.trim() ?? "unknown",
|
|
1137
|
+
optional: !!paramMatch[2] || !!paramMatch[4],
|
|
1138
|
+
defaultValue: paramMatch[4]?.trim(),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return params;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Split parameter string by commas, respecting nested angle brackets and parentheses.
|
|
1146
|
+
*/
|
|
1147
|
+
splitParams(raw) {
|
|
1148
|
+
const parts = [];
|
|
1149
|
+
let depth = 0;
|
|
1150
|
+
let current = "";
|
|
1151
|
+
for (const ch of raw) {
|
|
1152
|
+
if (ch === "<" || ch === "(" || ch === "[" || ch === "{") {
|
|
1153
|
+
depth++;
|
|
1154
|
+
current += ch;
|
|
1155
|
+
}
|
|
1156
|
+
else if (ch === ">" || ch === ")" || ch === "]" || ch === "}") {
|
|
1157
|
+
depth--;
|
|
1158
|
+
current += ch;
|
|
1159
|
+
}
|
|
1160
|
+
else if (ch === "," && depth === 0) {
|
|
1161
|
+
parts.push(current);
|
|
1162
|
+
current = "";
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
current += ch;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (current.trim())
|
|
1169
|
+
parts.push(current);
|
|
1170
|
+
return parts;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Clean a JSDoc comment — strip leading `*` and `/** ... * /`.
|
|
1174
|
+
*/
|
|
1175
|
+
cleanJsdoc(raw) {
|
|
1176
|
+
return raw
|
|
1177
|
+
.replace(/^\/\*\*\s*/, "")
|
|
1178
|
+
.replace(/\s*\*\/$/, "")
|
|
1179
|
+
.replace(/^\s*\*\s?/gm, "")
|
|
1180
|
+
.trim();
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Score a symbol against search tokens for relevance ranking.
|
|
1184
|
+
*/
|
|
1185
|
+
scoreSymbol(sym, name, tokens) {
|
|
1186
|
+
const nameLower = name.toLowerCase();
|
|
1187
|
+
// Split camelCase/PascalCase name into tokens
|
|
1188
|
+
const nameTokens = nameLower
|
|
1189
|
+
.replace(/([A-Z])/g, " $1")
|
|
1190
|
+
.toLowerCase()
|
|
1191
|
+
.split(/[\s_\-]+/)
|
|
1192
|
+
.filter(Boolean);
|
|
1193
|
+
const fileLower = relative(this.projectPath, sym.file).toLowerCase();
|
|
1194
|
+
const jsdocLower = sym.jsdoc?.toLowerCase() ?? "";
|
|
1195
|
+
const kindLower = sym.kind;
|
|
1196
|
+
let score = 0;
|
|
1197
|
+
let matchCount = 0;
|
|
1198
|
+
for (const token of tokens) {
|
|
1199
|
+
let matched = false;
|
|
1200
|
+
// Exact name match (highest score)
|
|
1201
|
+
if (nameLower === token) {
|
|
1202
|
+
score += 1.0;
|
|
1203
|
+
matched = true;
|
|
1204
|
+
}
|
|
1205
|
+
// Name contains token
|
|
1206
|
+
else if (nameLower.includes(token)) {
|
|
1207
|
+
score += 0.6;
|
|
1208
|
+
matched = true;
|
|
1209
|
+
}
|
|
1210
|
+
// Name token starts with query token
|
|
1211
|
+
else if (nameTokens.some((nt) => nt.startsWith(token))) {
|
|
1212
|
+
score += 0.4;
|
|
1213
|
+
matched = true;
|
|
1214
|
+
}
|
|
1215
|
+
// File path contains token
|
|
1216
|
+
if (fileLower.includes(token)) {
|
|
1217
|
+
score += 0.15;
|
|
1218
|
+
matched = true;
|
|
1219
|
+
}
|
|
1220
|
+
// JSDoc contains token
|
|
1221
|
+
if (jsdocLower.includes(token)) {
|
|
1222
|
+
score += 0.2;
|
|
1223
|
+
matched = true;
|
|
1224
|
+
}
|
|
1225
|
+
// Kind matches token
|
|
1226
|
+
if (kindLower === token) {
|
|
1227
|
+
score += 0.1;
|
|
1228
|
+
matched = true;
|
|
1229
|
+
}
|
|
1230
|
+
if (matched)
|
|
1231
|
+
matchCount++;
|
|
1232
|
+
}
|
|
1233
|
+
// Require at least one token to match
|
|
1234
|
+
if (matchCount === 0)
|
|
1235
|
+
return 0;
|
|
1236
|
+
// Bonus for matching all tokens
|
|
1237
|
+
if (matchCount === tokens.length && tokens.length > 1) {
|
|
1238
|
+
score *= 1.3;
|
|
1239
|
+
}
|
|
1240
|
+
// Bonus for exported symbols (more relevant)
|
|
1241
|
+
if (sym.exported)
|
|
1242
|
+
score *= 1.1;
|
|
1243
|
+
// Normalize to 0–1 range
|
|
1244
|
+
const maxPossible = tokens.length * 1.5 * 1.3 * 1.1;
|
|
1245
|
+
return Math.min(1, score / maxPossible);
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Get a code snippet around a symbol for search result display.
|
|
1249
|
+
*/
|
|
1250
|
+
getSnippet(sym) {
|
|
1251
|
+
const content = this.fileContents.get(sym.file);
|
|
1252
|
+
if (!content)
|
|
1253
|
+
return "";
|
|
1254
|
+
const lines = content.split("\n");
|
|
1255
|
+
const start = Math.max(0, sym.line - 1);
|
|
1256
|
+
const end = Math.min(lines.length, sym.line + 4); // 5 lines max
|
|
1257
|
+
return lines.slice(start, end).join("\n");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
//# sourceMappingURL=codebase-context.js.map
|