@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.
Files changed (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. 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