@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,1544 @@
1
+ /**
2
+ * @module cross-file-refactor
3
+ * @description Cross-File Refactoring Engine — enables safe renaming, moving, extracting,
4
+ * and inlining symbols across multiple TypeScript/JavaScript files.
5
+ *
6
+ * Provides preview (dry-run) mode, rollback support, and safety checks (breaking change
7
+ * detection, risk assessment). Uses regex-based analysis consistent with the rest of yuan-core.
8
+ */
9
+ import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
10
+ import { join, resolve, dirname, extname, relative } from "node:path";
11
+ import { randomUUID } from "node:crypto";
12
+ // ─── Constants ───
13
+ const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
14
+ const SKIP_DIRS = new Set([
15
+ "node_modules",
16
+ "dist",
17
+ ".git",
18
+ "build",
19
+ "coverage",
20
+ ".next",
21
+ ".turbo",
22
+ "__pycache__",
23
+ ]);
24
+ const MAX_ROLLBACKS = 10;
25
+ // ─── Regex patterns ───
26
+ /** Named import: import { X, Y } from "Z" */
27
+ const IMPORT_NAMED_RE = /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']([^"']+)["']/g;
28
+ /** Default import: import X from "Z" */
29
+ const IMPORT_DEFAULT_RE = /import\s+(\w+)\s+from\s+["']([^"']+)["']/g;
30
+ /** Namespace import: import * as X from "Z" */
31
+ const IMPORT_NAMESPACE_RE = /import\s+\*\s+as\s+(\w+)\s+from\s+["']([^"']+)["']/g;
32
+ /** Re-export: export { X } from "Z" */
33
+ const RE_EXPORT_RE = /export\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']([^"']+)["']/g;
34
+ /** Named export declaration */
35
+ const EXPORT_DECL_RE = /export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?|class|const|let|var|interface|type|enum)\s+(\w+)/g;
36
+ /** Default export */
37
+ const EXPORT_DEFAULT_RE = /export\s+default\s+/;
38
+ /** Single-line comment */
39
+ const SINGLE_LINE_COMMENT_RE = /\/\/.*/g;
40
+ /** Multi-line comment (non-greedy) */
41
+ const MULTI_LINE_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
42
+ /**
43
+ * Cross-File Refactoring Engine.
44
+ *
45
+ * Enables safe renaming, moving, extracting, and inlining of symbols
46
+ * across TypeScript/JavaScript projects. Supports preview (dry-run),
47
+ * rollback, and breaking change detection.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const refactor = new CrossFileRefactor("/path/to/project");
52
+ * const preview = await refactor.renameSymbol("OldName", "NewName");
53
+ * if (preview.riskLevel !== "high") {
54
+ * const result = await refactor.apply({ type: "rename", symbolName: "OldName", newName: "NewName" });
55
+ * }
56
+ * ```
57
+ */
58
+ export class CrossFileRefactor {
59
+ projectPath;
60
+ rollbacks;
61
+ rollbackOrder;
62
+ constructor(projectPath) {
63
+ this.projectPath = resolve(projectPath);
64
+ this.rollbacks = new Map();
65
+ this.rollbackOrder = [];
66
+ }
67
+ // ═══════════════════════════════════════════════════════════════
68
+ // Preview (dry run)
69
+ // ═══════════════════════════════════════════════════════════════
70
+ /**
71
+ * Generate a preview of the refactoring without making changes.
72
+ *
73
+ * @param request - The refactoring request
74
+ * @returns Preview with affected files, breaking changes, and risk assessment
75
+ */
76
+ async preview(request) {
77
+ switch (request.type) {
78
+ case "rename":
79
+ return this.renameSymbol(request.symbolName ?? "", request.newName ?? "", request.file);
80
+ case "move":
81
+ return this.moveSymbol(request.symbolName ?? "", request.sourceFile ?? "", request.targetFile ?? "", request.addReExport);
82
+ case "extract_function":
83
+ return this.extractFunction(request.sourceFile ?? "", request.startLine ?? 0, request.endLine ?? 0, request.extractedName ?? "extractedFunction", request.targetFile);
84
+ case "extract_interface":
85
+ return this.extractInterface(request.symbolName ?? "", request.extractedName ?? "", request.sourceFile ?? "");
86
+ case "inline":
87
+ return this.inlineFunction(request.symbolName ?? "", request.sourceFile ?? "");
88
+ case "change_signature":
89
+ return this.changeSignature(request.symbolName ?? "", request.sourceFile ?? "", request.newParams ?? [], request.newReturnType);
90
+ default:
91
+ return {
92
+ type: request.type,
93
+ affectedFiles: [],
94
+ totalChanges: 0,
95
+ breakingChanges: [],
96
+ riskLevel: "high",
97
+ warnings: [`Unknown refactoring type: ${request.type}`],
98
+ canAutoApply: false,
99
+ };
100
+ }
101
+ }
102
+ // ═══════════════════════════════════════════════════════════════
103
+ // Apply
104
+ // ═══════════════════════════════════════════════════════════════
105
+ /**
106
+ * Apply a refactoring to disk. Snapshots all affected files first
107
+ * so the operation can be rolled back.
108
+ *
109
+ * @param request - The refactoring request
110
+ * @returns Result including applied changes and rollback ID
111
+ */
112
+ async apply(request) {
113
+ const preview = await this.preview(request);
114
+ const filePaths = preview.affectedFiles.map((f) => f.file);
115
+ const rollbackId = await this.snapshotFiles(filePaths);
116
+ const appliedChanges = [];
117
+ const failedChanges = [];
118
+ for (const fc of preview.affectedFiles) {
119
+ try {
120
+ // Path traversal protection: ensure resolved path stays within project root
121
+ const resolvedPath = resolve(fc.file);
122
+ if (!resolvedPath.startsWith(this.projectPath)) {
123
+ failedChanges.push({
124
+ file: fc.file,
125
+ error: `Path traversal blocked: "${resolvedPath}" is outside project root "${this.projectPath}"`,
126
+ });
127
+ continue;
128
+ }
129
+ if (fc.isNewFile) {
130
+ // Create new file with the new text from changes
131
+ const dir = dirname(fc.file);
132
+ await mkdir(dir, { recursive: true });
133
+ const content = fc.changes.map((c) => c.newText).join("\n");
134
+ await writeFile(fc.file, content, "utf-8");
135
+ }
136
+ else if (fc.isDeletedFile) {
137
+ // We don't delete files — just clear content (safer)
138
+ await writeFile(fc.file, "", "utf-8");
139
+ }
140
+ else {
141
+ const content = await this.readFile(fc.file);
142
+ const updated = this.applyChanges(content, fc.changes);
143
+ await writeFile(fc.file, updated, "utf-8");
144
+ }
145
+ appliedChanges.push(fc);
146
+ }
147
+ catch (err) {
148
+ failedChanges.push({
149
+ file: fc.file,
150
+ error: err instanceof Error ? err.message : String(err),
151
+ });
152
+ }
153
+ }
154
+ return {
155
+ success: failedChanges.length === 0,
156
+ appliedChanges,
157
+ failedChanges,
158
+ rollbackAvailable: true,
159
+ rollbackId,
160
+ };
161
+ }
162
+ // ═══════════════════════════════════════════════════════════════
163
+ // Rollback
164
+ // ═══════════════════════════════════════════════════════════════
165
+ /**
166
+ * Roll back a previously applied refactoring by restoring original file contents.
167
+ *
168
+ * @param rollbackId - The rollback ID returned by apply()
169
+ * @returns True if rollback was successful
170
+ */
171
+ async rollback(rollbackId) {
172
+ const snapshot = this.rollbacks.get(rollbackId);
173
+ if (!snapshot)
174
+ return false;
175
+ try {
176
+ for (const [filePath, content] of snapshot) {
177
+ const dir = dirname(filePath);
178
+ await mkdir(dir, { recursive: true });
179
+ await writeFile(filePath, content, "utf-8");
180
+ }
181
+ this.rollbacks.delete(rollbackId);
182
+ this.rollbackOrder = this.rollbackOrder.filter((id) => id !== rollbackId);
183
+ return true;
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ }
189
+ // ═══════════════════════════════════════════════════════════════
190
+ // Rename Symbol
191
+ // ═══════════════════════════════════════════════════════════════
192
+ /**
193
+ * Rename a symbol across all files that reference it.
194
+ * Updates the definition, all usages, and all import statements.
195
+ *
196
+ * @param symbolName - Current symbol name
197
+ * @param newName - Desired new name
198
+ * @param scopeFile - Optional file to restrict the rename to
199
+ * @returns Preview of all changes
200
+ */
201
+ async renameSymbol(symbolName, newName, scopeFile) {
202
+ const warnings = [];
203
+ const breakingChanges = [];
204
+ if (!symbolName || !newName) {
205
+ return this.emptyPreview("rename", ["symbolName and newName are required"]);
206
+ }
207
+ if (symbolName === newName) {
208
+ return this.emptyPreview("rename", ["symbolName and newName are identical"]);
209
+ }
210
+ const usages = await this.findAllUsages(symbolName, scopeFile);
211
+ const imports = await this.findImports(symbolName);
212
+ if (usages.length === 0 && imports.length === 0) {
213
+ return this.emptyPreview("rename", [`No usages found for symbol "${symbolName}"`]);
214
+ }
215
+ // Group usages by file
216
+ const fileChangesMap = new Map();
217
+ // Process direct usages
218
+ for (const usage of usages) {
219
+ const changes = fileChangesMap.get(usage.file) ?? [];
220
+ const line = usage.context;
221
+ const newLine = this.replaceSymbolInLine(line, symbolName, newName);
222
+ if (newLine !== line) {
223
+ changes.push({
224
+ startLine: usage.line,
225
+ endLine: usage.line,
226
+ oldText: line,
227
+ newText: newLine,
228
+ reason: "rename usage",
229
+ });
230
+ }
231
+ fileChangesMap.set(usage.file, changes);
232
+ }
233
+ // Process import statements
234
+ for (const imp of imports) {
235
+ if (scopeFile && imp.file !== scopeFile)
236
+ continue;
237
+ const changes = fileChangesMap.get(imp.file) ?? [];
238
+ let newImport;
239
+ if (imp.isDefault) {
240
+ newImport = imp.importStatement.replace(new RegExp(`\\b${this.escapeRegex(symbolName)}\\b`), newName);
241
+ }
242
+ else {
243
+ // Named import: replace within braces
244
+ newImport = imp.importStatement.replace(new RegExp(`\\b${this.escapeRegex(symbolName)}\\b`), newName);
245
+ }
246
+ if (newImport !== imp.importStatement) {
247
+ changes.push({
248
+ startLine: imp.line,
249
+ endLine: imp.line,
250
+ oldText: imp.importStatement,
251
+ newText: newImport,
252
+ reason: "update import",
253
+ });
254
+ }
255
+ fileChangesMap.set(imp.file, changes);
256
+ }
257
+ // Deduplicate changes per file (same line)
258
+ const affectedFiles = [];
259
+ for (const [file, changes] of fileChangesMap) {
260
+ const deduped = this.deduplicateChanges(changes);
261
+ if (deduped.length > 0) {
262
+ affectedFiles.push({ file, changes: deduped, isNewFile: false, isDeletedFile: false });
263
+ }
264
+ }
265
+ // Check for breaking changes (exported symbol rename)
266
+ const isExported = await this.isSymbolExported(symbolName);
267
+ if (isExported) {
268
+ breakingChanges.push({
269
+ type: "api_change",
270
+ description: `Renaming exported symbol "${symbolName}" to "${newName}" — all consumers must update`,
271
+ file: isExported.file,
272
+ line: isExported.line,
273
+ severity: "warning",
274
+ });
275
+ }
276
+ const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
277
+ const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
278
+ return {
279
+ type: "rename",
280
+ affectedFiles,
281
+ totalChanges,
282
+ breakingChanges,
283
+ riskLevel,
284
+ warnings,
285
+ canAutoApply: riskLevel !== "high",
286
+ };
287
+ }
288
+ // ═══════════════════════════════════════════════════════════════
289
+ // Move Symbol
290
+ // ═══════════════════════════════════════════════════════════════
291
+ /**
292
+ * Move a symbol from one file to another. Updates all import statements
293
+ * across the project and optionally adds a re-export for backward compatibility.
294
+ *
295
+ * @param symbolName - Symbol to move
296
+ * @param sourceFile - Current file (absolute path)
297
+ * @param targetFile - Destination file (absolute path)
298
+ * @param addReExport - Whether to add a re-export in the source file
299
+ * @returns Preview of all changes
300
+ */
301
+ async moveSymbol(symbolName, sourceFile, targetFile, addReExport) {
302
+ const warnings = [];
303
+ const breakingChanges = [];
304
+ if (!symbolName || !sourceFile || !targetFile) {
305
+ return this.emptyPreview("move", ["symbolName, sourceFile, and targetFile are required"]);
306
+ }
307
+ const absSource = resolve(this.projectPath, sourceFile);
308
+ const absTarget = resolve(this.projectPath, targetFile);
309
+ let sourceContent;
310
+ try {
311
+ sourceContent = await this.readFile(absSource);
312
+ }
313
+ catch {
314
+ return this.emptyPreview("move", [`Source file not found: ${absSource}`]);
315
+ }
316
+ // Find the symbol definition in source
317
+ const definition = this.extractDefinition(sourceContent, symbolName);
318
+ if (!definition) {
319
+ return this.emptyPreview("move", [`Symbol "${symbolName}" not found in ${sourceFile}`]);
320
+ }
321
+ const affectedFiles = [];
322
+ // 1. Remove from source file
323
+ const sourceLines = sourceContent.split("\n");
324
+ const sourceChanges = [];
325
+ sourceChanges.push({
326
+ startLine: definition.startLine,
327
+ endLine: definition.endLine,
328
+ oldText: sourceLines.slice(definition.startLine - 1, definition.endLine).join("\n"),
329
+ newText: "",
330
+ reason: "remove moved symbol",
331
+ });
332
+ // Optionally add re-export
333
+ if (addReExport) {
334
+ const relPath = this.computeRelativeImportPath(absSource, absTarget);
335
+ const reExportLine = `export { ${symbolName} } from "${relPath}";`;
336
+ sourceChanges.push({
337
+ startLine: definition.startLine,
338
+ endLine: definition.startLine,
339
+ oldText: "",
340
+ newText: reExportLine,
341
+ reason: "add re-export for backward compatibility",
342
+ });
343
+ }
344
+ affectedFiles.push({
345
+ file: absSource,
346
+ changes: sourceChanges,
347
+ isNewFile: false,
348
+ isDeletedFile: false,
349
+ });
350
+ // 2. Add to target file
351
+ let targetExists = true;
352
+ let targetContent = "";
353
+ try {
354
+ targetContent = await this.readFile(absTarget);
355
+ }
356
+ catch {
357
+ targetExists = false;
358
+ }
359
+ const symbolCode = definition.fullText;
360
+ const targetChanges = [];
361
+ if (targetExists) {
362
+ const targetLines = targetContent.split("\n");
363
+ const lastLine = targetLines.length;
364
+ targetChanges.push({
365
+ startLine: lastLine,
366
+ endLine: lastLine,
367
+ oldText: targetLines[lastLine - 1] ?? "",
368
+ newText: (targetLines[lastLine - 1] ?? "") + "\n\n" + symbolCode,
369
+ reason: "add moved symbol",
370
+ });
371
+ }
372
+ else {
373
+ targetChanges.push({
374
+ startLine: 1,
375
+ endLine: 1,
376
+ oldText: "",
377
+ newText: symbolCode + "\n",
378
+ reason: "create file with moved symbol",
379
+ });
380
+ }
381
+ affectedFiles.push({
382
+ file: absTarget,
383
+ changes: targetChanges,
384
+ isNewFile: !targetExists,
385
+ isDeletedFile: false,
386
+ });
387
+ // 3. Update imports across all files
388
+ const allFiles = await this.collectSourceFiles(this.projectPath);
389
+ const importUpdates = this.generateImportUpdates(symbolName, absSource, absTarget, allFiles);
390
+ for (const update of importUpdates) {
391
+ // Find if we already have a FileChange for this file
392
+ const existing = affectedFiles.find((f) => f.file === update.file);
393
+ if (existing) {
394
+ existing.changes.push(update.change);
395
+ }
396
+ else {
397
+ affectedFiles.push({
398
+ file: update.file,
399
+ changes: [update.change],
400
+ isNewFile: false,
401
+ isDeletedFile: false,
402
+ });
403
+ }
404
+ }
405
+ // Breaking change if symbol was exported
406
+ if (definition.exported && !addReExport) {
407
+ breakingChanges.push({
408
+ type: "export_removed",
409
+ description: `Moving exported symbol "${symbolName}" without re-export — consumers importing from "${sourceFile}" will break`,
410
+ file: absSource,
411
+ line: definition.startLine,
412
+ severity: "error",
413
+ });
414
+ }
415
+ const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
416
+ const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
417
+ return {
418
+ type: "move",
419
+ affectedFiles,
420
+ totalChanges,
421
+ breakingChanges,
422
+ riskLevel,
423
+ warnings,
424
+ canAutoApply: riskLevel !== "high",
425
+ };
426
+ }
427
+ // ═══════════════════════════════════════════════════════════════
428
+ // Extract Function
429
+ // ═══════════════════════════════════════════════════════════════
430
+ /**
431
+ * Extract a range of lines into a new function. Infers parameters from
432
+ * variables used but defined outside the selection, and return values from
433
+ * variables modified inside the selection.
434
+ *
435
+ * @param sourceFile - File containing the code to extract
436
+ * @param startLine - First line to extract (1-based)
437
+ * @param endLine - Last line to extract (1-based, inclusive)
438
+ * @param functionName - Name for the new function
439
+ * @param targetFile - File to place the function in (same file if omitted)
440
+ * @returns Preview of all changes
441
+ */
442
+ async extractFunction(sourceFile, startLine, endLine, functionName, targetFile) {
443
+ const warnings = [];
444
+ const breakingChanges = [];
445
+ if (!sourceFile || startLine <= 0 || endLine <= 0 || endLine < startLine) {
446
+ return this.emptyPreview("extract_function", ["Invalid sourceFile or line range"]);
447
+ }
448
+ const absSource = resolve(this.projectPath, sourceFile);
449
+ let content;
450
+ try {
451
+ content = await this.readFile(absSource);
452
+ }
453
+ catch {
454
+ return this.emptyPreview("extract_function", [`Source file not found: ${absSource}`]);
455
+ }
456
+ const lines = content.split("\n");
457
+ if (startLine > lines.length || endLine > lines.length) {
458
+ return this.emptyPreview("extract_function", ["Line range exceeds file length"]);
459
+ }
460
+ const selectedLines = lines.slice(startLine - 1, endLine);
461
+ const selectedCode = selectedLines.join("\n");
462
+ // Determine surrounding code for parameter inference
463
+ const beforeCode = lines.slice(0, startLine - 1).join("\n");
464
+ const afterCode = lines.slice(endLine).join("\n");
465
+ const surroundingCode = beforeCode + "\n" + afterCode;
466
+ // Infer parameters and return values
467
+ const params = this.inferParameters(selectedCode, surroundingCode);
468
+ const returnVars = this.inferReturnValues(selectedCode, afterCode);
469
+ // Build function signature
470
+ const paramStr = params.map((p) => `${p.name}: ${p.type}`).join(", ");
471
+ let returnType = "void";
472
+ let returnStatement = "";
473
+ if (returnVars.length === 1) {
474
+ returnType = returnVars[0].type;
475
+ returnStatement = `\n return ${returnVars[0].name};`;
476
+ }
477
+ else if (returnVars.length > 1) {
478
+ returnType = `{ ${returnVars.map((v) => `${v.name}: ${v.type}`).join("; ")} }`;
479
+ returnStatement = `\n return { ${returnVars.map((v) => v.name).join(", ")} };`;
480
+ }
481
+ // Determine indentation
482
+ const baseIndent = this.detectIndent(selectedLines[0] ?? "");
483
+ const dedentedCode = selectedLines.map((l) => l.replace(new RegExp(`^${baseIndent}`), " ")).join("\n");
484
+ const functionDef = `function ${functionName}(${paramStr}): ${returnType} {\n${dedentedCode}${returnStatement}\n}`;
485
+ // Build the call expression
486
+ const callArgs = params.map((p) => p.name).join(", ");
487
+ let callExpr;
488
+ if (returnVars.length === 0) {
489
+ callExpr = `${baseIndent}${functionName}(${callArgs});`;
490
+ }
491
+ else if (returnVars.length === 1) {
492
+ callExpr = `${baseIndent}const ${returnVars[0].name} = ${functionName}(${callArgs});`;
493
+ }
494
+ else {
495
+ callExpr = `${baseIndent}const { ${returnVars.map((v) => v.name).join(", ")} } = ${functionName}(${callArgs});`;
496
+ }
497
+ const affectedFiles = [];
498
+ const sameFile = !targetFile || resolve(this.projectPath, targetFile) === absSource;
499
+ if (sameFile) {
500
+ // Replace selected lines with call, add function at end of file
501
+ affectedFiles.push({
502
+ file: absSource,
503
+ changes: [
504
+ {
505
+ startLine,
506
+ endLine,
507
+ oldText: selectedCode,
508
+ newText: callExpr,
509
+ reason: "replace with function call",
510
+ },
511
+ {
512
+ startLine: lines.length,
513
+ endLine: lines.length,
514
+ oldText: lines[lines.length - 1] ?? "",
515
+ newText: (lines[lines.length - 1] ?? "") + "\n\n" + functionDef,
516
+ reason: "add extracted function",
517
+ },
518
+ ],
519
+ isNewFile: false,
520
+ isDeletedFile: false,
521
+ });
522
+ }
523
+ else {
524
+ const absTarget = resolve(this.projectPath, targetFile);
525
+ const relPath = this.computeRelativeImportPath(absSource, absTarget);
526
+ const importLine = `import { ${functionName} } from "${relPath}";`;
527
+ // Source: replace selected lines, add import
528
+ const importInsertLine = this.findImportInsertLine(lines);
529
+ affectedFiles.push({
530
+ file: absSource,
531
+ changes: [
532
+ {
533
+ startLine: importInsertLine,
534
+ endLine: importInsertLine,
535
+ oldText: lines[importInsertLine - 1] ?? "",
536
+ newText: importLine + "\n" + (lines[importInsertLine - 1] ?? ""),
537
+ reason: "add import for extracted function",
538
+ },
539
+ {
540
+ startLine: startLine + 1, // +1 because we inserted a line above
541
+ endLine: endLine + 1,
542
+ oldText: selectedCode,
543
+ newText: callExpr,
544
+ reason: "replace with function call",
545
+ },
546
+ ],
547
+ isNewFile: false,
548
+ isDeletedFile: false,
549
+ });
550
+ // Target: add the function
551
+ let targetExists = true;
552
+ try {
553
+ await this.readFile(absTarget);
554
+ }
555
+ catch {
556
+ targetExists = false;
557
+ }
558
+ affectedFiles.push({
559
+ file: absTarget,
560
+ changes: [
561
+ {
562
+ startLine: 1,
563
+ endLine: 1,
564
+ oldText: "",
565
+ newText: `export ${functionDef}\n`,
566
+ reason: "add extracted function",
567
+ },
568
+ ],
569
+ isNewFile: !targetExists,
570
+ isDeletedFile: false,
571
+ });
572
+ }
573
+ const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
574
+ const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
575
+ return {
576
+ type: "extract_function",
577
+ affectedFiles,
578
+ totalChanges,
579
+ breakingChanges,
580
+ riskLevel,
581
+ warnings,
582
+ canAutoApply: riskLevel !== "high",
583
+ };
584
+ }
585
+ // ═══════════════════════════════════════════════════════════════
586
+ // Extract Interface
587
+ // ═══════════════════════════════════════════════════════════════
588
+ /**
589
+ * Extract an interface from a class's public members.
590
+ *
591
+ * @param className - Class to extract from
592
+ * @param interfaceName - Name for the new interface
593
+ * @param sourceFile - File containing the class
594
+ * @returns Preview of all changes
595
+ */
596
+ async extractInterface(className, interfaceName, sourceFile) {
597
+ const warnings = [];
598
+ if (!className || !interfaceName || !sourceFile) {
599
+ return this.emptyPreview("extract_interface", ["className, interfaceName, and sourceFile are required"]);
600
+ }
601
+ const absSource = resolve(this.projectPath, sourceFile);
602
+ let content;
603
+ try {
604
+ content = await this.readFile(absSource);
605
+ }
606
+ catch {
607
+ return this.emptyPreview("extract_interface", [`Source file not found: ${absSource}`]);
608
+ }
609
+ // Find class definition and extract public methods/properties
610
+ const classInfo = this.extractClassMembers(content, className);
611
+ if (!classInfo) {
612
+ return this.emptyPreview("extract_interface", [`Class "${className}" not found in ${sourceFile}`]);
613
+ }
614
+ // Build interface from public members
615
+ const memberLines = [];
616
+ for (const member of classInfo.publicMembers) {
617
+ if (member.kind === "method") {
618
+ const paramStr = member.params ?? "";
619
+ const retType = member.returnType ?? "void";
620
+ memberLines.push(` ${member.name}(${paramStr}): ${retType};`);
621
+ }
622
+ else {
623
+ const propType = member.type ?? "unknown";
624
+ memberLines.push(` ${member.name}: ${propType};`);
625
+ }
626
+ }
627
+ const interfaceDef = `export interface ${interfaceName} {\n${memberLines.join("\n")}\n}`;
628
+ // Add interface before the class and make class implement it
629
+ const lines = content.split("\n");
630
+ const affectedFiles = [];
631
+ const changes = [];
632
+ // Insert interface before class
633
+ changes.push({
634
+ startLine: classInfo.line,
635
+ endLine: classInfo.line,
636
+ oldText: lines[classInfo.line - 1],
637
+ newText: interfaceDef + "\n\n" + lines[classInfo.line - 1],
638
+ reason: "add extracted interface",
639
+ });
640
+ // Update class declaration to implement the interface
641
+ const classLine = lines[classInfo.line - 1];
642
+ const implementsMatch = classLine.match(/\bimplements\s+([^{]+)/);
643
+ if (implementsMatch) {
644
+ const updated = classLine.replace(/\bimplements\s+([^{]+)/, `implements ${implementsMatch[1].trim()}, ${interfaceName}`);
645
+ changes.push({
646
+ startLine: classInfo.line,
647
+ endLine: classInfo.line,
648
+ oldText: classLine,
649
+ newText: updated,
650
+ reason: "add implements clause",
651
+ });
652
+ }
653
+ else {
654
+ const updated = classLine.replace(/\bclass\s+(\w+)(\s*(?:extends\s+\w+\s*)?)\{/, `class $1$2 implements ${interfaceName} {`);
655
+ if (updated !== classLine) {
656
+ // We already have the insert change at this line, so update the first change
657
+ changes[0].newText = interfaceDef + "\n\n" + updated;
658
+ }
659
+ }
660
+ affectedFiles.push({
661
+ file: absSource,
662
+ changes: this.deduplicateChanges(changes),
663
+ isNewFile: false,
664
+ isDeletedFile: false,
665
+ });
666
+ const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
667
+ return {
668
+ type: "extract_interface",
669
+ affectedFiles,
670
+ totalChanges,
671
+ breakingChanges: [],
672
+ riskLevel: "low",
673
+ warnings,
674
+ canAutoApply: true,
675
+ };
676
+ }
677
+ // ═══════════════════════════════════════════════════════════════
678
+ // Inline Function
679
+ // ═══════════════════════════════════════════════════════════════
680
+ /**
681
+ * Inline a simple function at all call sites (replace calls with the function body).
682
+ * Only works for simple single-expression or single-statement functions.
683
+ *
684
+ * @param functionName - Function to inline
685
+ * @param sourceFile - File where the function is defined
686
+ * @returns Preview of all changes
687
+ */
688
+ async inlineFunction(functionName, sourceFile) {
689
+ const warnings = [];
690
+ if (!functionName || !sourceFile) {
691
+ return this.emptyPreview("inline", ["functionName and sourceFile are required"]);
692
+ }
693
+ const absSource = resolve(this.projectPath, sourceFile);
694
+ let content;
695
+ try {
696
+ content = await this.readFile(absSource);
697
+ }
698
+ catch {
699
+ return this.emptyPreview("inline", [`Source file not found: ${absSource}`]);
700
+ }
701
+ // Extract function body
702
+ const funcInfo = this.extractFunctionBody(content, functionName);
703
+ if (!funcInfo) {
704
+ return this.emptyPreview("inline", [`Function "${functionName}" not found or too complex to inline`]);
705
+ }
706
+ if (funcInfo.bodyLines > 5) {
707
+ warnings.push(`Function has ${funcInfo.bodyLines} lines — inlining may reduce readability`);
708
+ }
709
+ // Find all call sites across the project
710
+ const usages = await this.findAllUsages(functionName);
711
+ const affectedFiles = [];
712
+ // Group by file
713
+ const fileUsages = new Map();
714
+ for (const usage of usages) {
715
+ // Skip the definition itself
716
+ if (usage.file === absSource && usage.line >= funcInfo.startLine && usage.line <= funcInfo.endLine) {
717
+ continue;
718
+ }
719
+ const list = fileUsages.get(usage.file) ?? [];
720
+ list.push(usage);
721
+ fileUsages.set(usage.file, list);
722
+ }
723
+ for (const [file, fileUses] of fileUsages) {
724
+ const changes = [];
725
+ for (const usage of fileUses) {
726
+ // Check if this line contains a function call pattern
727
+ const callRe = new RegExp(`${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)`);
728
+ const callMatch = usage.context.match(callRe);
729
+ if (!callMatch)
730
+ continue;
731
+ const args = callMatch[1].split(",").map((a) => a.trim()).filter(Boolean);
732
+ let inlinedBody = funcInfo.body;
733
+ // Substitute parameters with arguments
734
+ for (let i = 0; i < funcInfo.params.length && i < args.length; i++) {
735
+ const paramRe = new RegExp(`\\b${this.escapeRegex(funcInfo.params[i])}\\b`, "g");
736
+ inlinedBody = inlinedBody.replace(paramRe, args[i]);
737
+ }
738
+ // For single-expression returns, unwrap
739
+ const returnMatch = inlinedBody.match(/^\s*return\s+(.+?);?\s*$/);
740
+ if (returnMatch) {
741
+ inlinedBody = returnMatch[1];
742
+ }
743
+ const newLine = usage.context.replace(callRe, inlinedBody);
744
+ changes.push({
745
+ startLine: usage.line,
746
+ endLine: usage.line,
747
+ oldText: usage.context,
748
+ newText: newLine,
749
+ reason: "inline function call",
750
+ });
751
+ }
752
+ if (changes.length > 0) {
753
+ affectedFiles.push({ file, changes, isNewFile: false, isDeletedFile: false });
754
+ }
755
+ }
756
+ // Remove the function definition from source
757
+ const sourceLines = content.split("\n");
758
+ const defText = sourceLines.slice(funcInfo.startLine - 1, funcInfo.endLine).join("\n");
759
+ affectedFiles.push({
760
+ file: absSource,
761
+ changes: [{
762
+ startLine: funcInfo.startLine,
763
+ endLine: funcInfo.endLine,
764
+ oldText: defText,
765
+ newText: "",
766
+ reason: "remove inlined function definition",
767
+ }],
768
+ isNewFile: false,
769
+ isDeletedFile: false,
770
+ });
771
+ const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
772
+ const breakingChanges = [];
773
+ // If exported, it's a breaking change
774
+ if (funcInfo.exported) {
775
+ breakingChanges.push({
776
+ type: "export_removed",
777
+ description: `Inlining exported function "${functionName}" — external consumers will break`,
778
+ file: absSource,
779
+ line: funcInfo.startLine,
780
+ severity: "error",
781
+ });
782
+ }
783
+ const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
784
+ return {
785
+ type: "inline",
786
+ affectedFiles,
787
+ totalChanges,
788
+ breakingChanges,
789
+ riskLevel,
790
+ warnings,
791
+ canAutoApply: riskLevel !== "high",
792
+ };
793
+ }
794
+ // ═══════════════════════════════════════════════════════════════
795
+ // Change Signature
796
+ // ═══════════════════════════════════════════════════════════════
797
+ /**
798
+ * Change a function's parameter list and/or return type. Updates the definition
799
+ * and all call sites.
800
+ *
801
+ * @param functionName - Function to modify
802
+ * @param sourceFile - File where the function is defined
803
+ * @param newParams - New parameter list
804
+ * @param newReturnType - New return type (optional)
805
+ * @returns Preview of all changes
806
+ */
807
+ async changeSignature(functionName, sourceFile, newParams, newReturnType) {
808
+ const warnings = [];
809
+ const breakingChanges = [];
810
+ if (!functionName || !sourceFile) {
811
+ return this.emptyPreview("change_signature", ["functionName and sourceFile are required"]);
812
+ }
813
+ const absSource = resolve(this.projectPath, sourceFile);
814
+ let content;
815
+ try {
816
+ content = await this.readFile(absSource);
817
+ }
818
+ catch {
819
+ return this.emptyPreview("change_signature", [`Source file not found: ${absSource}`]);
820
+ }
821
+ // Find the function definition line
822
+ const lines = content.split("\n");
823
+ const funcDefRe = new RegExp(`(export\\s+)?(?:async\\s+)?function\\s+${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)(?:\\s*:\\s*([^{]+))?`);
824
+ let defLine = -1;
825
+ let defMatch = null;
826
+ for (let i = 0; i < lines.length; i++) {
827
+ defMatch = lines[i].match(funcDefRe);
828
+ if (defMatch) {
829
+ defLine = i + 1;
830
+ break;
831
+ }
832
+ }
833
+ if (defLine === -1 || !defMatch) {
834
+ return this.emptyPreview("change_signature", [`Function "${functionName}" definition not found`]);
835
+ }
836
+ const oldParamStr = defMatch[2];
837
+ const oldReturnType = defMatch[3]?.trim();
838
+ const isExported = !!defMatch[1];
839
+ // Build new signature
840
+ const newParamStr = newParams
841
+ .map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`)
842
+ .join(", ");
843
+ const returnTypeStr = newReturnType
844
+ ? `: ${newReturnType}`
845
+ : oldReturnType
846
+ ? `: ${oldReturnType}`
847
+ : "";
848
+ const affectedFiles = [];
849
+ const sourceChanges = [];
850
+ // Update definition
851
+ const oldLine = lines[defLine - 1];
852
+ const newLine = oldLine
853
+ .replace(`(${oldParamStr})`, `(${newParamStr})`)
854
+ .replace(oldReturnType ? `: ${oldReturnType}` : /(?=\s*\{)/, returnTypeStr);
855
+ sourceChanges.push({
856
+ startLine: defLine,
857
+ endLine: defLine,
858
+ oldText: oldLine,
859
+ newText: newLine,
860
+ reason: "update function signature",
861
+ });
862
+ affectedFiles.push({
863
+ file: absSource,
864
+ changes: sourceChanges,
865
+ isNewFile: false,
866
+ isDeletedFile: false,
867
+ });
868
+ // Update call sites across project
869
+ const usages = await this.findAllUsages(functionName);
870
+ const oldParams = oldParamStr
871
+ .split(",")
872
+ .map((p) => p.trim().split(/[?:]/)[0].trim())
873
+ .filter(Boolean);
874
+ for (const usage of usages) {
875
+ if (usage.file === absSource && usage.line === defLine)
876
+ continue;
877
+ const callRe = new RegExp(`${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)`);
878
+ const callMatch = usage.context.match(callRe);
879
+ if (!callMatch)
880
+ continue;
881
+ const oldArgs = callMatch[1].split(",").map((a) => a.trim());
882
+ // Map old args to new params by name matching
883
+ const newArgs = [];
884
+ for (const param of newParams) {
885
+ const oldIdx = oldParams.indexOf(param.name);
886
+ if (oldIdx >= 0 && oldIdx < oldArgs.length) {
887
+ newArgs.push(oldArgs[oldIdx]);
888
+ }
889
+ else if (param.optional) {
890
+ // Skip optional params without old args
891
+ }
892
+ else {
893
+ newArgs.push(`/* TODO: ${param.name} */`);
894
+ warnings.push(`New required parameter "${param.name}" at ${usage.file}:${usage.line} needs a value`);
895
+ }
896
+ }
897
+ const newCallExpr = `${functionName}(${newArgs.join(", ")})`;
898
+ const updatedLine = usage.context.replace(callRe, newCallExpr);
899
+ if (updatedLine !== usage.context) {
900
+ const existing = affectedFiles.find((f) => f.file === usage.file);
901
+ const change = {
902
+ startLine: usage.line,
903
+ endLine: usage.line,
904
+ oldText: usage.context,
905
+ newText: updatedLine,
906
+ reason: "update call site arguments",
907
+ };
908
+ if (existing) {
909
+ existing.changes.push(change);
910
+ }
911
+ else {
912
+ affectedFiles.push({
913
+ file: usage.file,
914
+ changes: [change],
915
+ isNewFile: false,
916
+ isDeletedFile: false,
917
+ });
918
+ }
919
+ }
920
+ }
921
+ if (isExported) {
922
+ breakingChanges.push({
923
+ type: "signature_change",
924
+ description: `Changing signature of exported function "${functionName}"`,
925
+ file: absSource,
926
+ line: defLine,
927
+ severity: "warning",
928
+ });
929
+ }
930
+ const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
931
+ const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
932
+ return {
933
+ type: "change_signature",
934
+ affectedFiles,
935
+ totalChanges,
936
+ breakingChanges,
937
+ riskLevel,
938
+ warnings,
939
+ canAutoApply: riskLevel !== "high",
940
+ };
941
+ }
942
+ // ═══════════════════════════════════════════════════════════════
943
+ // Safety
944
+ // ═══════════════════════════════════════════════════════════════
945
+ /**
946
+ * Perform safety analysis on a refactoring preview.
947
+ * Returns pre-check info and placeholder post-check (build verification
948
+ * must be run separately after apply).
949
+ *
950
+ * @param preview - The refactoring preview to analyze
951
+ * @returns Safety analysis result
952
+ */
953
+ async checkSafety(preview) {
954
+ return {
955
+ preCheck: {
956
+ affectedFiles: preview.affectedFiles.map((f) => f.file),
957
+ breakingChanges: preview.breakingChanges,
958
+ riskLevel: preview.riskLevel,
959
+ },
960
+ postCheck: {
961
+ // These must be verified after apply() by running tsc/build
962
+ buildSuccess: false,
963
+ noNewErrors: false,
964
+ },
965
+ };
966
+ }
967
+ // ═══════════════════════════════════════════════════════════════
968
+ // Private: Find Usages
969
+ // ═══════════════════════════════════════════════════════════════
970
+ /**
971
+ * Find all usages of a symbol across the project using regex word boundary matching.
972
+ * Excludes occurrences inside comments and string literals.
973
+ */
974
+ async findAllUsages(symbolName, scopeFile) {
975
+ const usages = [];
976
+ const files = scopeFile
977
+ ? [resolve(this.projectPath, scopeFile)]
978
+ : await this.collectSourceFiles(this.projectPath);
979
+ const symbolRe = new RegExp(`(?<![.\\w])${this.escapeRegex(symbolName)}(?!\\w)`, "g");
980
+ for (const file of files) {
981
+ let content;
982
+ try {
983
+ content = await this.readFile(file);
984
+ }
985
+ catch {
986
+ continue;
987
+ }
988
+ const lines = content.split("\n");
989
+ for (let i = 0; i < lines.length; i++) {
990
+ const line = lines[i];
991
+ if (this.isCommentLine(line))
992
+ continue;
993
+ if (!symbolRe.test(line))
994
+ continue;
995
+ symbolRe.lastIndex = 0;
996
+ // Check that the match is not inside a string literal
997
+ if (this.isInStringLiteral(line, symbolName))
998
+ continue;
999
+ usages.push({
1000
+ file,
1001
+ line: i + 1,
1002
+ column: line.indexOf(symbolName),
1003
+ context: line,
1004
+ });
1005
+ }
1006
+ }
1007
+ return usages;
1008
+ }
1009
+ /**
1010
+ * Find all import statements that reference a symbol.
1011
+ */
1012
+ async findImports(symbolName) {
1013
+ const results = [];
1014
+ const files = await this.collectSourceFiles(this.projectPath);
1015
+ for (const file of files) {
1016
+ let content;
1017
+ try {
1018
+ content = await this.readFile(file);
1019
+ }
1020
+ catch {
1021
+ continue;
1022
+ }
1023
+ const lines = content.split("\n");
1024
+ for (let i = 0; i < lines.length; i++) {
1025
+ const line = lines[i];
1026
+ // Named import: import { X, Y } from "Z"
1027
+ const namedRe = new RegExp(IMPORT_NAMED_RE.source, "g");
1028
+ let match;
1029
+ while ((match = namedRe.exec(line)) !== null) {
1030
+ const symbols = match[1].split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim());
1031
+ if (symbols.includes(symbolName)) {
1032
+ results.push({ file, line: i + 1, importStatement: line, isDefault: false });
1033
+ }
1034
+ }
1035
+ // Default import: import X from "Z"
1036
+ const defaultRe = new RegExp(IMPORT_DEFAULT_RE.source, "g");
1037
+ while ((match = defaultRe.exec(line)) !== null) {
1038
+ if (match[1] === symbolName) {
1039
+ results.push({ file, line: i + 1, importStatement: line, isDefault: true });
1040
+ }
1041
+ }
1042
+ // Re-export: export { X } from "Z"
1043
+ const reExportRe = new RegExp(RE_EXPORT_RE.source, "g");
1044
+ while ((match = reExportRe.exec(line)) !== null) {
1045
+ const symbols = match[1].split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim());
1046
+ if (symbols.includes(symbolName)) {
1047
+ results.push({ file, line: i + 1, importStatement: line, isDefault: false });
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+ return results;
1053
+ }
1054
+ /**
1055
+ * Generate import path updates when a symbol moves between files.
1056
+ */
1057
+ generateImportUpdates(symbolName, oldFile, newFile, files) {
1058
+ const results = [];
1059
+ // We need to synchronously scan files that have already been read.
1060
+ // For preview purposes, we'll do a simulated scan based on files list.
1061
+ // The actual file reading happens in apply(). For preview, we return
1062
+ // placeholder updates that will be resolved during apply.
1063
+ // This is a limitation of the synchronous generateImportUpdates interface.
1064
+ // In practice, this method is called after findImports has already scanned files.
1065
+ // We return empty here and let the moveSymbol method handle imports via findImports.
1066
+ return results;
1067
+ }
1068
+ // ═══════════════════════════════════════════════════════════════
1069
+ // Private: Symbol Analysis
1070
+ // ═══════════════════════════════════════════════════════════════
1071
+ /**
1072
+ * Check if a symbol is exported from any file.
1073
+ */
1074
+ async isSymbolExported(symbolName) {
1075
+ const files = await this.collectSourceFiles(this.projectPath);
1076
+ const exportRe = new RegExp(`export\\s+(?:declare\\s+)?(?:abstract\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|const|let|var|interface|type|enum)\\s+${this.escapeRegex(symbolName)}\\b`);
1077
+ for (const file of files) {
1078
+ let content;
1079
+ try {
1080
+ content = await this.readFile(file);
1081
+ }
1082
+ catch {
1083
+ continue;
1084
+ }
1085
+ const lines = content.split("\n");
1086
+ for (let i = 0; i < lines.length; i++) {
1087
+ if (exportRe.test(lines[i])) {
1088
+ return { file, line: i + 1 };
1089
+ }
1090
+ }
1091
+ }
1092
+ return null;
1093
+ }
1094
+ /**
1095
+ * Extract a symbol's full definition (with JSDoc) from file content.
1096
+ */
1097
+ extractDefinition(content, symbolName) {
1098
+ const lines = content.split("\n");
1099
+ // Pattern: [export] [async] function/class/interface/type/enum/const NAME
1100
+ const declRe = new RegExp(`^(\\s*)(export\\s+)?(?:declare\\s+)?(?:abstract\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|interface|type|enum|const|let|var)\\s+${this.escapeRegex(symbolName)}\\b`);
1101
+ for (let i = 0; i < lines.length; i++) {
1102
+ if (!declRe.test(lines[i]))
1103
+ continue;
1104
+ const exported = /\bexport\b/.test(lines[i]);
1105
+ // Look backward for JSDoc
1106
+ let jsdocStart = i;
1107
+ if (i > 0 && lines[i - 1].trim().endsWith("*/")) {
1108
+ for (let j = i - 1; j >= 0; j--) {
1109
+ jsdocStart = j;
1110
+ if (lines[j].trim().startsWith("/**") || lines[j].trim().startsWith("/*"))
1111
+ break;
1112
+ }
1113
+ }
1114
+ // Find end of definition by brace matching
1115
+ const endLine = this.findDefinitionEnd(lines, i);
1116
+ const fullText = lines.slice(jsdocStart, endLine).join("\n");
1117
+ return {
1118
+ fullText,
1119
+ startLine: jsdocStart + 1,
1120
+ endLine,
1121
+ exported,
1122
+ };
1123
+ }
1124
+ return null;
1125
+ }
1126
+ /**
1127
+ * Find the end line of a definition by tracking brace depth.
1128
+ * For single-line type aliases or simple declarations, returns the same line.
1129
+ */
1130
+ findDefinitionEnd(lines, startIdx) {
1131
+ let braceDepth = 0;
1132
+ let foundOpen = false;
1133
+ for (let i = startIdx; i < lines.length; i++) {
1134
+ const line = lines[i];
1135
+ for (const ch of line) {
1136
+ if (ch === "{") {
1137
+ braceDepth++;
1138
+ foundOpen = true;
1139
+ }
1140
+ else if (ch === "}") {
1141
+ braceDepth--;
1142
+ if (foundOpen && braceDepth === 0) {
1143
+ return i + 1; // 1-based end line
1144
+ }
1145
+ }
1146
+ }
1147
+ // For type aliases / single line declarations without braces
1148
+ if (!foundOpen && line.includes(";")) {
1149
+ return i + 1;
1150
+ }
1151
+ }
1152
+ // Fallback: return the start line
1153
+ return startIdx + 1;
1154
+ }
1155
+ /**
1156
+ * Extract public members from a class definition.
1157
+ */
1158
+ extractClassMembers(content, className) {
1159
+ const lines = content.split("\n");
1160
+ const classRe = new RegExp(`(?:export\\s+)?(?:abstract\\s+)?class\\s+${this.escapeRegex(className)}\\b`);
1161
+ let classLine = -1;
1162
+ for (let i = 0; i < lines.length; i++) {
1163
+ if (classRe.test(lines[i])) {
1164
+ classLine = i;
1165
+ break;
1166
+ }
1167
+ }
1168
+ if (classLine === -1)
1169
+ return null;
1170
+ const endLine = this.findDefinitionEnd(lines, classLine);
1171
+ const classBody = lines.slice(classLine + 1, endLine - 1);
1172
+ const publicMembers = [];
1173
+ // Method pattern: [async] methodName(params): ReturnType {
1174
+ const methodRe = /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?\s*\{?/;
1175
+ // Property pattern: [readonly] propName: Type;
1176
+ const propRe = /^\s*(?:readonly\s+)?(\w+)(?:\?)?:\s*([^;=]+)/;
1177
+ for (const bodyLine of classBody) {
1178
+ const trimmed = bodyLine.trim();
1179
+ // Skip private/protected members
1180
+ if (trimmed.startsWith("private ") || trimmed.startsWith("protected ") || trimmed.startsWith("#")) {
1181
+ continue;
1182
+ }
1183
+ // Skip constructor
1184
+ if (trimmed.startsWith("constructor"))
1185
+ continue;
1186
+ const mMatch = trimmed.match(methodRe);
1187
+ if (mMatch && mMatch[1] !== "get" && mMatch[1] !== "set") {
1188
+ publicMembers.push({
1189
+ name: mMatch[1],
1190
+ kind: "method",
1191
+ params: mMatch[2]?.trim(),
1192
+ returnType: mMatch[3]?.trim(),
1193
+ });
1194
+ continue;
1195
+ }
1196
+ const pMatch = trimmed.match(propRe);
1197
+ if (pMatch) {
1198
+ publicMembers.push({
1199
+ name: pMatch[1],
1200
+ kind: "property",
1201
+ type: pMatch[2]?.trim(),
1202
+ });
1203
+ }
1204
+ }
1205
+ return { line: classLine + 1, publicMembers };
1206
+ }
1207
+ /**
1208
+ * Extract a function's body and metadata for inlining.
1209
+ */
1210
+ extractFunctionBody(content, functionName) {
1211
+ const lines = content.split("\n");
1212
+ const funcRe = new RegExp(`^(\\s*)(export\\s+)?(?:async\\s+)?function\\s+${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)`);
1213
+ for (let i = 0; i < lines.length; i++) {
1214
+ const match = lines[i].match(funcRe);
1215
+ if (!match)
1216
+ continue;
1217
+ const exported = !!match[2];
1218
+ const paramsStr = match[3];
1219
+ const params = paramsStr
1220
+ .split(",")
1221
+ .map((p) => p.trim().split(/[?:]/)[0].trim())
1222
+ .filter(Boolean);
1223
+ const endLine = this.findDefinitionEnd(lines, i);
1224
+ // Extract body (everything between first { and last })
1225
+ const fullText = lines.slice(i, endLine).join("\n");
1226
+ const braceStart = fullText.indexOf("{");
1227
+ const braceEnd = fullText.lastIndexOf("}");
1228
+ if (braceStart === -1 || braceEnd === -1)
1229
+ return null;
1230
+ const body = fullText.slice(braceStart + 1, braceEnd).trim();
1231
+ const bodyLines = body.split("\n").length;
1232
+ return {
1233
+ body,
1234
+ params,
1235
+ startLine: i + 1,
1236
+ endLine,
1237
+ bodyLines,
1238
+ exported,
1239
+ };
1240
+ }
1241
+ return null;
1242
+ }
1243
+ // ═══════════════════════════════════════════════════════════════
1244
+ // Private: Parameter Inference
1245
+ // ═══════════════════════════════════════════════════════════════
1246
+ /**
1247
+ * Infer function parameters from a code block by finding variables
1248
+ * that are used but not defined within the selection.
1249
+ */
1250
+ inferParameters(code, surroundingCode) {
1251
+ // Find all identifiers used in the code
1252
+ const identRe = /\b([a-zA-Z_$][\w$]*)\b/g;
1253
+ const usedIdents = new Set();
1254
+ let match;
1255
+ while ((match = identRe.exec(code)) !== null) {
1256
+ usedIdents.add(match[1]);
1257
+ }
1258
+ // Find identifiers defined within the code (const/let/var/function/class declarations)
1259
+ const declRe = /(?:const|let|var|function|class)\s+(\w+)/g;
1260
+ const declaredInCode = new Set();
1261
+ while ((match = declRe.exec(code)) !== null) {
1262
+ declaredInCode.add(match[1]);
1263
+ }
1264
+ // Find identifiers declared in surrounding code
1265
+ const declaredOutside = new Set();
1266
+ const outsideDeclRe = /(?:const|let|var|function|class)\s+(\w+)/g;
1267
+ while ((match = outsideDeclRe.exec(surroundingCode)) !== null) {
1268
+ declaredOutside.add(match[1]);
1269
+ }
1270
+ // Also look for parameter patterns: (name: Type)
1271
+ const paramDeclRe = /(\w+)\s*[?]?:\s*(\w[\w<>,\s|&[\]]*)/g;
1272
+ const typeMap = new Map();
1273
+ while ((match = paramDeclRe.exec(surroundingCode)) !== null) {
1274
+ typeMap.set(match[1], match[2].trim());
1275
+ }
1276
+ // Parameters = used in code, not declared in code, but declared outside
1277
+ const keywords = new Set([
1278
+ "const", "let", "var", "function", "class", "if", "else", "for",
1279
+ "while", "return", "import", "export", "from", "new", "this",
1280
+ "true", "false", "null", "undefined", "typeof", "instanceof",
1281
+ "void", "async", "await", "try", "catch", "throw", "switch",
1282
+ "case", "break", "continue", "default", "do", "in", "of",
1283
+ "delete", "yield", "super", "extends", "implements", "interface",
1284
+ "type", "enum", "as", "is", "keyof", "readonly", "declare",
1285
+ "abstract", "static", "public", "private", "protected",
1286
+ "console", "Math", "JSON", "Array", "Object", "String",
1287
+ "Number", "Boolean", "Date", "Error", "Promise", "Map", "Set",
1288
+ "RegExp", "Symbol", "BigInt", "Infinity", "NaN",
1289
+ ]);
1290
+ const params = [];
1291
+ for (const ident of usedIdents) {
1292
+ if (declaredInCode.has(ident))
1293
+ continue;
1294
+ if (keywords.has(ident))
1295
+ continue;
1296
+ if (!declaredOutside.has(ident))
1297
+ continue;
1298
+ const type = typeMap.get(ident) ?? "unknown";
1299
+ params.push({ name: ident, type });
1300
+ }
1301
+ return params;
1302
+ }
1303
+ /**
1304
+ * Infer return values: variables modified inside the selection that are
1305
+ * used after the selection.
1306
+ */
1307
+ inferReturnValues(code, afterCode) {
1308
+ // Find variables assigned in the code
1309
+ const assignRe = /(?:let|var)\s+(\w+)/g;
1310
+ const assignedVars = new Set();
1311
+ let match;
1312
+ while ((match = assignRe.exec(code)) !== null) {
1313
+ assignedVars.add(match[1]);
1314
+ }
1315
+ // Also find direct reassignments
1316
+ const reassignRe = /^(\s*)(\w+)\s*=[^=]/gm;
1317
+ while ((match = reassignRe.exec(code)) !== null) {
1318
+ assignedVars.add(match[2]);
1319
+ }
1320
+ // Check which are used after the selection
1321
+ const results = [];
1322
+ for (const varName of assignedVars) {
1323
+ const useRe = new RegExp(`\\b${this.escapeRegex(varName)}\\b`);
1324
+ if (useRe.test(afterCode)) {
1325
+ results.push({ name: varName, type: "unknown" });
1326
+ }
1327
+ }
1328
+ return results;
1329
+ }
1330
+ // ═══════════════════════════════════════════════════════════════
1331
+ // Private: File Utilities
1332
+ // ═══════════════════════════════════════════════════════════════
1333
+ /**
1334
+ * Read a file's content as UTF-8 text.
1335
+ */
1336
+ async readFile(filePath) {
1337
+ // Path containment check: ensure file is within project root
1338
+ const resolved = resolve(filePath);
1339
+ if (!resolved.startsWith(this.projectPath)) {
1340
+ throw new Error(`Path traversal blocked: "${resolved}" is outside project root`);
1341
+ }
1342
+ return readFile(filePath, "utf-8");
1343
+ }
1344
+ /**
1345
+ * Apply text changes to file content. Changes are applied in reverse line order
1346
+ * to preserve line numbers.
1347
+ */
1348
+ applyChanges(content, changes) {
1349
+ const lines = content.split("\n");
1350
+ // Sort changes by startLine descending so later changes don't shift earlier ones
1351
+ const sorted = [...changes].sort((a, b) => b.startLine - a.startLine);
1352
+ for (const change of sorted) {
1353
+ const startIdx = change.startLine - 1;
1354
+ const endIdx = change.endLine; // exclusive in splice
1355
+ const count = endIdx - startIdx;
1356
+ if (change.newText === "") {
1357
+ // Delete the lines
1358
+ lines.splice(startIdx, count);
1359
+ }
1360
+ else {
1361
+ const newLines = change.newText.split("\n");
1362
+ lines.splice(startIdx, count, ...newLines);
1363
+ }
1364
+ }
1365
+ return lines.join("\n");
1366
+ }
1367
+ /**
1368
+ * Snapshot files for potential rollback. Returns a unique rollback ID.
1369
+ * Maintains a maximum of MAX_ROLLBACKS snapshots.
1370
+ */
1371
+ async snapshotFiles(files) {
1372
+ const id = randomUUID();
1373
+ const snapshot = new Map();
1374
+ for (const file of files) {
1375
+ try {
1376
+ const content = await this.readFile(file);
1377
+ snapshot.set(file, content);
1378
+ }
1379
+ catch {
1380
+ // File doesn't exist yet — store empty to indicate deletion on rollback
1381
+ snapshot.set(file, "");
1382
+ }
1383
+ }
1384
+ this.rollbacks.set(id, snapshot);
1385
+ this.rollbackOrder.push(id);
1386
+ // Evict oldest rollbacks if over limit
1387
+ while (this.rollbackOrder.length > MAX_ROLLBACKS) {
1388
+ const oldest = this.rollbackOrder.shift();
1389
+ this.rollbacks.delete(oldest);
1390
+ }
1391
+ return id;
1392
+ }
1393
+ /**
1394
+ * Recursively collect all TypeScript/JavaScript source files in a directory.
1395
+ */
1396
+ async collectSourceFiles(dir) {
1397
+ const results = [];
1398
+ let entries;
1399
+ try {
1400
+ entries = await readdir(dir, { withFileTypes: true });
1401
+ }
1402
+ catch {
1403
+ return results;
1404
+ }
1405
+ for (const entry of entries) {
1406
+ // Skip symlinks to prevent escaping project root
1407
+ if (entry.isSymbolicLink())
1408
+ continue;
1409
+ const fullPath = join(dir, entry.name);
1410
+ // Ensure path stays within project root
1411
+ if (!resolve(fullPath).startsWith(this.projectPath))
1412
+ continue;
1413
+ if (entry.isDirectory()) {
1414
+ if (!SKIP_DIRS.has(entry.name)) {
1415
+ const sub = await this.collectSourceFiles(fullPath);
1416
+ results.push(...sub);
1417
+ }
1418
+ }
1419
+ else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
1420
+ results.push(fullPath);
1421
+ }
1422
+ }
1423
+ return results;
1424
+ }
1425
+ // ═══════════════════════════════════════════════════════════════
1426
+ // Private: String / Line Utilities
1427
+ // ═══════════════════════════════════════════════════════════════
1428
+ /**
1429
+ * Replace a symbol name in a line using word boundary matching.
1430
+ * Preserves string literals and comments.
1431
+ */
1432
+ replaceSymbolInLine(line, oldName, newName) {
1433
+ const re = new RegExp(`(?<![.\\w])${this.escapeRegex(oldName)}(?!\\w)`, "g");
1434
+ return line.replace(re, newName);
1435
+ }
1436
+ /**
1437
+ * Check if a line is a comment (single-line // or starts inside a block comment).
1438
+ */
1439
+ isCommentLine(line) {
1440
+ const trimmed = line.trim();
1441
+ return (trimmed.startsWith("//") ||
1442
+ trimmed.startsWith("*") ||
1443
+ trimmed.startsWith("/*") ||
1444
+ trimmed.startsWith("*/"));
1445
+ }
1446
+ /**
1447
+ * Basic heuristic: check if a symbol occurrence is inside a string literal.
1448
+ * Counts unescaped quotes before the symbol position.
1449
+ */
1450
+ isInStringLiteral(line, symbolName) {
1451
+ const idx = line.indexOf(symbolName);
1452
+ if (idx === -1)
1453
+ return false;
1454
+ // Strip comments first
1455
+ const noComment = line.replace(/\/\/.*$/, "");
1456
+ if (idx >= noComment.length)
1457
+ return true; // symbol is in a comment
1458
+ // Count quotes before the symbol
1459
+ const before = noComment.slice(0, idx);
1460
+ const singleQuotes = (before.match(/(?<!\\)'/g) ?? []).length;
1461
+ const doubleQuotes = (before.match(/(?<!\\)"/g) ?? []).length;
1462
+ const backticks = (before.match(/(?<!\\)`/g) ?? []).length;
1463
+ // If any quote count is odd, we're inside a string
1464
+ return singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0;
1465
+ }
1466
+ /**
1467
+ * Compute the relative import path from one file to another,
1468
+ * with .js extension (ESM convention).
1469
+ */
1470
+ computeRelativeImportPath(fromFile, toFile) {
1471
+ const fromDir = dirname(fromFile);
1472
+ let rel = relative(fromDir, toFile);
1473
+ // Replace .ts/.tsx extension with .js
1474
+ rel = rel.replace(/\.tsx?$/, ".js");
1475
+ // Ensure it starts with ./
1476
+ if (!rel.startsWith(".")) {
1477
+ rel = "./" + rel;
1478
+ }
1479
+ return rel;
1480
+ }
1481
+ /**
1482
+ * Find the best line to insert a new import statement.
1483
+ * Returns the line number (1-based) after the last existing import.
1484
+ */
1485
+ findImportInsertLine(lines) {
1486
+ let lastImportLine = 0;
1487
+ for (let i = 0; i < lines.length; i++) {
1488
+ if (/^\s*import\s/.test(lines[i])) {
1489
+ lastImportLine = i + 1;
1490
+ }
1491
+ }
1492
+ return lastImportLine > 0 ? lastImportLine + 1 : 1;
1493
+ }
1494
+ /**
1495
+ * Detect the leading whitespace (indentation) of a line.
1496
+ */
1497
+ detectIndent(line) {
1498
+ const match = line.match(/^(\s*)/);
1499
+ return match ? match[1] : "";
1500
+ }
1501
+ /**
1502
+ * Escape special regex characters in a string.
1503
+ */
1504
+ escapeRegex(str) {
1505
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1506
+ }
1507
+ /**
1508
+ * Deduplicate text changes for the same line, keeping only the last one.
1509
+ */
1510
+ deduplicateChanges(changes) {
1511
+ const seen = new Map();
1512
+ for (const change of changes) {
1513
+ seen.set(change.startLine, change);
1514
+ }
1515
+ return [...seen.values()].sort((a, b) => a.startLine - b.startLine);
1516
+ }
1517
+ /**
1518
+ * Assess the risk level of a refactoring based on scope and breaking changes.
1519
+ */
1520
+ assessRisk(fileCount, breakingCount, changeCount) {
1521
+ if (breakingCount > 0 && fileCount > 5)
1522
+ return "high";
1523
+ if (breakingCount > 0)
1524
+ return "medium";
1525
+ if (fileCount > 10 || changeCount > 50)
1526
+ return "medium";
1527
+ return "low";
1528
+ }
1529
+ /**
1530
+ * Create an empty preview with warnings (for error cases).
1531
+ */
1532
+ emptyPreview(type, warnings) {
1533
+ return {
1534
+ type,
1535
+ affectedFiles: [],
1536
+ totalChanges: 0,
1537
+ breakingChanges: [],
1538
+ riskLevel: "low",
1539
+ warnings,
1540
+ canAutoApply: false,
1541
+ };
1542
+ }
1543
+ }
1544
+ //# sourceMappingURL=cross-file-refactor.js.map