@yuaone/core 0.4.1 → 0.4.3

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 (186) hide show
  1. package/dist/agent-logger.d.ts +1 -1
  2. package/dist/agent-logger.d.ts.map +1 -1
  3. package/dist/agent-logger.js +17 -15
  4. package/dist/agent-logger.js.map +1 -1
  5. package/dist/agent-loop.d.ts +31 -0
  6. package/dist/agent-loop.d.ts.map +1 -1
  7. package/dist/agent-loop.js +514 -98
  8. package/dist/agent-loop.js.map +1 -1
  9. package/dist/agent-modes.d.ts.map +1 -1
  10. package/dist/agent-modes.js +5 -0
  11. package/dist/agent-modes.js.map +1 -1
  12. package/dist/async-completion-queue.d.ts +2 -0
  13. package/dist/async-completion-queue.d.ts.map +1 -1
  14. package/dist/async-completion-queue.js +14 -0
  15. package/dist/async-completion-queue.js.map +1 -1
  16. package/dist/auto-fix.d.ts.map +1 -1
  17. package/dist/auto-fix.js +12 -1
  18. package/dist/auto-fix.js.map +1 -1
  19. package/dist/benchmark-runner.d.ts.map +1 -1
  20. package/dist/benchmark-runner.js +5 -1
  21. package/dist/benchmark-runner.js.map +1 -1
  22. package/dist/constants.d.ts +12 -0
  23. package/dist/constants.d.ts.map +1 -1
  24. package/dist/constants.js +14 -0
  25. package/dist/constants.js.map +1 -1
  26. package/dist/context-manager.d.ts +25 -0
  27. package/dist/context-manager.d.ts.map +1 -1
  28. package/dist/context-manager.js +132 -5
  29. package/dist/context-manager.js.map +1 -1
  30. package/dist/continuation-engine.d.ts.map +1 -1
  31. package/dist/continuation-engine.js +8 -7
  32. package/dist/continuation-engine.js.map +1 -1
  33. package/dist/continuous-reflection.d.ts.map +1 -1
  34. package/dist/continuous-reflection.js +22 -12
  35. package/dist/continuous-reflection.js.map +1 -1
  36. package/dist/cost-optimizer.js +1 -1
  37. package/dist/cost-optimizer.js.map +1 -1
  38. package/dist/cross-file-refactor.d.ts.map +1 -1
  39. package/dist/cross-file-refactor.js +7 -2
  40. package/dist/cross-file-refactor.js.map +1 -1
  41. package/dist/dag-orchestrator.d.ts +10 -1
  42. package/dist/dag-orchestrator.d.ts.map +1 -1
  43. package/dist/dag-orchestrator.js +101 -6
  44. package/dist/dag-orchestrator.js.map +1 -1
  45. package/dist/debate-orchestrator.d.ts +1 -0
  46. package/dist/debate-orchestrator.d.ts.map +1 -1
  47. package/dist/debate-orchestrator.js +27 -15
  48. package/dist/debate-orchestrator.js.map +1 -1
  49. package/dist/dependency-analyzer.d.ts.map +1 -1
  50. package/dist/dependency-analyzer.js +19 -1
  51. package/dist/dependency-analyzer.js.map +1 -1
  52. package/dist/dynamic-role-generator.d.ts.map +1 -1
  53. package/dist/dynamic-role-generator.js +6 -3
  54. package/dist/dynamic-role-generator.js.map +1 -1
  55. package/dist/errors.js +1 -1
  56. package/dist/errors.js.map +1 -1
  57. package/dist/event-bus.d.ts.map +1 -1
  58. package/dist/event-bus.js +4 -3
  59. package/dist/event-bus.js.map +1 -1
  60. package/dist/execution-engine.d.ts +39 -1
  61. package/dist/execution-engine.d.ts.map +1 -1
  62. package/dist/execution-engine.js +453 -83
  63. package/dist/execution-engine.js.map +1 -1
  64. package/dist/failure-recovery.d.ts.map +1 -1
  65. package/dist/failure-recovery.js +14 -3
  66. package/dist/failure-recovery.js.map +1 -1
  67. package/dist/git-intelligence.d.ts.map +1 -1
  68. package/dist/git-intelligence.js +16 -11
  69. package/dist/git-intelligence.js.map +1 -1
  70. package/dist/governor.d.ts +8 -0
  71. package/dist/governor.d.ts.map +1 -1
  72. package/dist/governor.js +19 -1
  73. package/dist/governor.js.map +1 -1
  74. package/dist/hierarchical-planner.d.ts +3 -0
  75. package/dist/hierarchical-planner.d.ts.map +1 -1
  76. package/dist/hierarchical-planner.js +32 -2
  77. package/dist/hierarchical-planner.js.map +1 -1
  78. package/dist/impact-analyzer.d.ts +27 -0
  79. package/dist/impact-analyzer.d.ts.map +1 -1
  80. package/dist/impact-analyzer.js +415 -53
  81. package/dist/impact-analyzer.js.map +1 -1
  82. package/dist/intent-inference.d.ts.map +1 -1
  83. package/dist/intent-inference.js +20 -24
  84. package/dist/intent-inference.js.map +1 -1
  85. package/dist/kernel.d.ts.map +1 -1
  86. package/dist/kernel.js +5 -3
  87. package/dist/kernel.js.map +1 -1
  88. package/dist/language-detector.d.ts +19 -0
  89. package/dist/language-detector.d.ts.map +1 -0
  90. package/dist/language-detector.js +482 -0
  91. package/dist/language-detector.js.map +1 -0
  92. package/dist/language-support.d.ts.map +1 -1
  93. package/dist/language-support.js +5 -9
  94. package/dist/language-support.js.map +1 -1
  95. package/dist/llm-client.d.ts +21 -8
  96. package/dist/llm-client.d.ts.map +1 -1
  97. package/dist/llm-client.js +125 -21
  98. package/dist/llm-client.js.map +1 -1
  99. package/dist/mcp-client.d.ts.map +1 -1
  100. package/dist/mcp-client.js +9 -1
  101. package/dist/mcp-client.js.map +1 -1
  102. package/dist/memory-manager.d.ts +13 -8
  103. package/dist/memory-manager.d.ts.map +1 -1
  104. package/dist/memory-manager.js +125 -32
  105. package/dist/memory-manager.js.map +1 -1
  106. package/dist/memory-updater.d.ts.map +1 -1
  107. package/dist/memory-updater.js +5 -4
  108. package/dist/memory-updater.js.map +1 -1
  109. package/dist/memory.d.ts +6 -2
  110. package/dist/memory.d.ts.map +1 -1
  111. package/dist/memory.js +32 -4
  112. package/dist/memory.js.map +1 -1
  113. package/dist/parallel-executor.d.ts +7 -0
  114. package/dist/parallel-executor.d.ts.map +1 -1
  115. package/dist/parallel-executor.js +28 -0
  116. package/dist/parallel-executor.js.map +1 -1
  117. package/dist/perf-optimizer.d.ts.map +1 -1
  118. package/dist/perf-optimizer.js +18 -3
  119. package/dist/perf-optimizer.js.map +1 -1
  120. package/dist/persona.d.ts.map +1 -1
  121. package/dist/persona.js +8 -3
  122. package/dist/persona.js.map +1 -1
  123. package/dist/planner.d.ts.map +1 -1
  124. package/dist/planner.js +5 -3
  125. package/dist/planner.js.map +1 -1
  126. package/dist/plugin-auto-loader.d.ts.map +1 -1
  127. package/dist/plugin-auto-loader.js +4 -1
  128. package/dist/plugin-auto-loader.js.map +1 -1
  129. package/dist/plugin-registry.d.ts +4 -0
  130. package/dist/plugin-registry.d.ts.map +1 -1
  131. package/dist/plugin-registry.js +6 -0
  132. package/dist/plugin-registry.js.map +1 -1
  133. package/dist/plugin-validator.d.ts.map +1 -1
  134. package/dist/plugin-validator.js +10 -1
  135. package/dist/plugin-validator.js.map +1 -1
  136. package/dist/reasoning-aggregator.d.ts +35 -0
  137. package/dist/reasoning-aggregator.d.ts.map +1 -0
  138. package/dist/reasoning-aggregator.js +102 -0
  139. package/dist/reasoning-aggregator.js.map +1 -0
  140. package/dist/reasoning-tree.d.ts +23 -0
  141. package/dist/reasoning-tree.d.ts.map +1 -0
  142. package/dist/reasoning-tree.js +44 -0
  143. package/dist/reasoning-tree.js.map +1 -0
  144. package/dist/session-persistence.d.ts +8 -4
  145. package/dist/session-persistence.d.ts.map +1 -1
  146. package/dist/session-persistence.js +22 -7
  147. package/dist/session-persistence.js.map +1 -1
  148. package/dist/skill-learner.d.ts.map +1 -1
  149. package/dist/skill-learner.js +4 -2
  150. package/dist/skill-learner.js.map +1 -1
  151. package/dist/skill-loader.d.ts +4 -0
  152. package/dist/skill-loader.d.ts.map +1 -1
  153. package/dist/skill-loader.js +6 -0
  154. package/dist/skill-loader.js.map +1 -1
  155. package/dist/speculative-executor.d.ts +22 -0
  156. package/dist/speculative-executor.d.ts.map +1 -1
  157. package/dist/speculative-executor.js +90 -45
  158. package/dist/speculative-executor.js.map +1 -1
  159. package/dist/state-machine.d.ts.map +1 -1
  160. package/dist/state-machine.js +4 -2
  161. package/dist/state-machine.js.map +1 -1
  162. package/dist/sub-agent-prompts.d.ts +5 -29
  163. package/dist/sub-agent-prompts.d.ts.map +1 -1
  164. package/dist/sub-agent-prompts.js +231 -134
  165. package/dist/sub-agent-prompts.js.map +1 -1
  166. package/dist/sub-agent.d.ts +19 -0
  167. package/dist/sub-agent.d.ts.map +1 -1
  168. package/dist/sub-agent.js +135 -11
  169. package/dist/sub-agent.js.map +1 -1
  170. package/dist/system-prompt.d.ts.map +1 -1
  171. package/dist/system-prompt.js +45 -0
  172. package/dist/system-prompt.js.map +1 -1
  173. package/dist/task-classifier.js +1 -1
  174. package/dist/task-classifier.js.map +1 -1
  175. package/dist/types.d.ts +67 -1
  176. package/dist/types.d.ts.map +1 -1
  177. package/dist/types.js.map +1 -1
  178. package/dist/vector-index.d.ts +14 -0
  179. package/dist/vector-index.d.ts.map +1 -1
  180. package/dist/vector-index.js +84 -16
  181. package/dist/vector-index.js.map +1 -1
  182. package/dist/workspace-lock.d.ts +5 -0
  183. package/dist/workspace-lock.d.ts.map +1 -0
  184. package/dist/workspace-lock.js +16 -0
  185. package/dist/workspace-lock.js.map +1 -0
  186. package/package.json +1 -1
@@ -8,7 +8,7 @@
8
8
  * Integrates with CodebaseContext, TestIntelligence, and CrossFileRefactor.
9
9
  */
10
10
  import { readFile } from "node:fs/promises";
11
- import { join, basename, dirname, extname, relative } from "node:path";
11
+ import { join, basename, dirname, extname } from "node:path";
12
12
  import { execFile } from "node:child_process";
13
13
  import { promisify } from "node:util";
14
14
  const execFileAsync = promisify(execFile);
@@ -30,6 +30,9 @@ const CONFIG_FILE_PATTERNS = [
30
30
  const DOC_FILE_PATTERNS = [/\.md$/, /\.mdx$/, /\.txt$/, /CHANGELOG/];
31
31
  const API_ENDPOINT_PATTERN = /\.(get|post|put|patch|delete|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/g;
32
32
  const FUNCTION_SIGNATURE_PATTERN = /export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
33
+ const TYPE_EXPORT_PATTERN = /export\s+(?:type|interface)\s+(\w+)/g;
34
+ const SYMBOL_USAGE_PATTERN = /\b([A-Za-z_][A-Za-z0-9_]*)\b/g;
35
+ const FUNCTION_CALL_PATTERN = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
33
36
  // ─── Helpers ───
34
37
  function classifyFile(filePath) {
35
38
  if (TEST_FILE_PATTERNS.some((p) => p.test(filePath)))
@@ -40,11 +43,23 @@ function classifyFile(filePath) {
40
43
  return "doc";
41
44
  return "source";
42
45
  }
46
+ function extractTypeExports(content) {
47
+ const out = [];
48
+ const re = new RegExp(TYPE_EXPORT_PATTERN.source, "g");
49
+ let m;
50
+ while ((m = re.exec(content)) !== null) {
51
+ out.push(m[1]);
52
+ }
53
+ return out;
54
+ }
43
55
  function fileNameWithoutExt(filePath) {
44
56
  const base = basename(filePath);
45
57
  const ext = extname(base);
46
58
  return base.slice(0, -ext.length);
47
59
  }
60
+ function resolveProjectPath(projectPath, filePath) {
61
+ return filePath.startsWith("/") ? filePath : join(projectPath, filePath);
62
+ }
48
63
  function extractExports(content) {
49
64
  const exports = [];
50
65
  const re = new RegExp(EXPORT_PATTERN.source, "g");
@@ -54,6 +69,24 @@ function extractExports(content) {
54
69
  }
55
70
  return exports;
56
71
  }
72
+ function extractSymbolUsage(content) {
73
+ const out = [];
74
+ const re = new RegExp(SYMBOL_USAGE_PATTERN.source, "g");
75
+ let m;
76
+ while ((m = re.exec(content)) !== null) {
77
+ out.push(m[1]);
78
+ }
79
+ return out;
80
+ }
81
+ function extractFunctionCalls(content) {
82
+ const out = [];
83
+ const re = new RegExp(FUNCTION_CALL_PATTERN.source, "g");
84
+ let m;
85
+ while ((m = re.exec(content)) !== null) {
86
+ out.push(m[1]);
87
+ }
88
+ return out;
89
+ }
57
90
  function extractImportPaths(content) {
58
91
  const paths = [];
59
92
  const re = new RegExp(IMPORT_FROM_PATTERN.source, "g");
@@ -101,6 +134,8 @@ async function walkDir(dir, maxFiles = 5000) {
101
134
  const files = [];
102
135
  const queue = [dir];
103
136
  while (queue.length > 0 && files.length < maxFiles) {
137
+ if (queue.length > 20000)
138
+ break;
104
139
  const current = queue.shift();
105
140
  try {
106
141
  const entries = await readdir(current, { withFileTypes: true });
@@ -130,6 +165,7 @@ export class ImpactAnalyzer {
130
165
  includeTests;
131
166
  includeAPIs;
132
167
  strictMode;
168
+ symbolGraph;
133
169
  constructor(config) {
134
170
  this.projectPath = config.projectPath;
135
171
  this.maxDepth = config.maxDepth ?? 3;
@@ -142,24 +178,36 @@ export class ImpactAnalyzer {
142
178
  */
143
179
  async analyzeChanges(changedFiles) {
144
180
  try {
145
- const [affectedFiles, affectedTests, affectedAPIs, breakingChanges] = await Promise.all([
181
+ const graph = await this.getGraph();
182
+ await graph.update(changedFiles.map((f) => resolveProjectPath(this.projectPath, f)));
183
+ const cycles = graph.detectCycles();
184
+ const [affectedFiles, affectedTests, affectedAPIs, breakingChanges, deadCodeCandidates, testCoverage, refactorPlan,] = await Promise.all([
146
185
  this.collectAffectedFiles(changedFiles),
147
186
  this.includeTests ? this.suggestTests(changedFiles) : Promise.resolve([]),
148
187
  this.includeAPIs ? this.collectAffectedAPIs(changedFiles) : Promise.resolve([]),
149
188
  this.detectBreaking(changedFiles),
189
+ this.detectDeadCode(changedFiles),
190
+ this.inferTestCoverage(changedFiles),
191
+ this.buildSafeRefactorPlan(changedFiles),
150
192
  ]);
151
193
  const riskLevel = this.estimateRisk({
152
194
  files: changedFiles,
153
195
  linesChanged: affectedFiles.length * 20, // rough estimate
154
196
  });
155
- const summary = this.buildSummary(changedFiles, affectedFiles, affectedTests, breakingChanges, riskLevel);
156
- const suggestedActions = this.buildSuggestedActions(affectedTests, breakingChanges, riskLevel);
197
+ let summary = this.buildSummary(changedFiles, affectedFiles, affectedTests, breakingChanges, riskLevel);
198
+ if (cycles.length > 0) {
199
+ summary += ` ${cycles.length} dependency cycle(s) detected.`;
200
+ }
201
+ const suggestedActions = this.buildSuggestedActions(affectedTests, breakingChanges, riskLevel, deadCodeCandidates, testCoverage);
157
202
  return {
158
203
  changedFiles,
159
204
  affectedFiles,
160
205
  affectedTests,
161
206
  affectedAPIs,
162
207
  breakingChanges,
208
+ deadCodeCandidates,
209
+ testCoverage,
210
+ refactorPlan,
163
211
  riskLevel,
164
212
  summary,
165
213
  suggestedActions,
@@ -171,6 +219,9 @@ export class ImpactAnalyzer {
171
219
  affectedFiles: [],
172
220
  affectedTests: [],
173
221
  affectedAPIs: [],
222
+ deadCodeCandidates: [],
223
+ testCoverage: [],
224
+ refactorPlan: [],
174
225
  breakingChanges: [],
175
226
  riskLevel: "minimal",
176
227
  summary: "Impact analysis could not be completed.",
@@ -183,38 +234,27 @@ export class ImpactAnalyzer {
183
234
  */
184
235
  async findAffectedFiles(filePath) {
185
236
  try {
186
- const content = await readFileSafe(filePath);
237
+ const absoluteFilePath = resolveProjectPath(this.projectPath, filePath);
238
+ const content = await readFileSafe(absoluteFilePath);
187
239
  if (!content)
188
240
  return [];
189
241
  const exports = extractExports(content);
190
- const changedName = fileNameWithoutExt(filePath);
191
- const projectFiles = await walkDir(this.projectPath);
242
+ const changedName = basename(absoluteFilePath).replace(/\.[jt]sx?$/, "");
243
+ const graph = await this.getGraph();
192
244
  const affected = [];
193
245
  const seen = new Set();
194
246
  // BFS through import graph
195
247
  const queue = [];
196
- // Find direct importers
197
- for (const pf of projectFiles) {
198
- if (pf === filePath)
199
- continue;
200
- const pfContent = await readFileSafe(pf);
201
- if (!pfContent)
248
+ const importers = graph.reverseImports.get(changedName) ?? new Set();
249
+ for (const importer of importers) {
250
+ if (seen.has(importer) || importer === absoluteFilePath)
202
251
  continue;
203
- const importPaths = extractImportPaths(pfContent);
204
- const importsChanged = importPaths.some((ip) => {
205
- const importBase = basename(ip).replace(/\.[jt]sx?$/, "");
206
- return (ip.includes(changedName) ||
207
- importBase === changedName ||
208
- ip.endsWith(relative(this.projectPath, filePath).replace(/\.[jt]sx?$/, "")));
252
+ queue.push({
253
+ file: importer,
254
+ depth: 1,
255
+ reason: `imports changed module "${changedName}"`,
256
+ confidence: 0.95,
209
257
  });
210
- if (importsChanged) {
211
- queue.push({
212
- file: pf,
213
- depth: 1,
214
- reason: `imports changed module "${changedName}"`,
215
- confidence: 0.95,
216
- });
217
- }
218
258
  }
219
259
  // Process queue up to maxDepth
220
260
  while (queue.length > 0) {
@@ -230,38 +270,30 @@ export class ImpactAnalyzer {
230
270
  type: fileType,
231
271
  });
232
272
  if (depth < this.maxDepth) {
233
- const transitiveName = fileNameWithoutExt(file);
234
- for (const pf of projectFiles) {
235
- if (seen.has(pf) || pf === file)
273
+ const transitiveKey = basename(file).replace(/\.[jt]sx?$/, "");
274
+ const nextImporters = graph.reverseImports.get(transitiveKey) ?? new Set();
275
+ for (const pf of nextImporters) {
276
+ if (seen.has(pf))
236
277
  continue;
237
- const pfContent = await readFileSafe(pf);
238
- if (!pfContent)
239
- continue;
240
- const importPaths = extractImportPaths(pfContent);
241
- if (importPaths.some((ip) => ip.includes(transitiveName))) {
242
- queue.push({
243
- file: pf,
244
- depth: depth + 1,
245
- reason: `transitively affected via "${transitiveName}" (depth ${depth + 1})`,
246
- confidence: confidence * 0.7,
247
- });
248
- }
278
+ queue.push({
279
+ file: pf,
280
+ depth: depth + 1,
281
+ reason: `transitively affected via "${transitiveKey}"`,
282
+ confidence: confidence * 0.7,
283
+ });
249
284
  }
250
285
  }
251
286
  }
252
287
  // If an export is a function/class used in test files, flag those too
253
288
  if (this.includeTests) {
254
- for (const pf of projectFiles) {
255
- if (seen.has(pf) || !TEST_FILE_PATTERNS.some((p) => p.test(pf)))
256
- continue;
257
- const pfContent = await readFileSafe(pf);
258
- if (!pfContent)
289
+ for (const [pf, pfContent] of graph.fileContents.entries()) {
290
+ if (seen.has(pf) || pf === absoluteFilePath)
259
291
  continue;
260
- const usesExport = exports.some((exp) => pfContent.includes(exp));
292
+ const usesExport = exports.some((exp) => new RegExp(`\\b${exp}\\b`).test(pfContent));
261
293
  if (usesExport) {
262
294
  affected.push({
263
295
  path: pf,
264
- reason: `test for changed file "${changedName}"`,
296
+ reason: `test for changed module "${changedName}"`,
265
297
  confidence: 0.8,
266
298
  type: "test",
267
299
  });
@@ -307,6 +339,7 @@ export class ImpactAnalyzer {
307
339
  */
308
340
  async suggestTests(changedFiles) {
309
341
  try {
342
+ const graph = await this.getGraph();
310
343
  const projectFiles = await walkDir(this.projectPath);
311
344
  const testFiles = projectFiles.filter((f) => TEST_FILE_PATTERNS.some((p) => p.test(f)));
312
345
  const tests = [];
@@ -343,7 +376,7 @@ export class ImpactAnalyzer {
343
376
  continue;
344
377
  }
345
378
  // Test imports the changed module
346
- const tfContent = await readFileSafe(tf);
379
+ const tfContent = graph.fileContents.get(tf) ?? await readFileSafe(tf);
347
380
  if (!tfContent)
348
381
  continue;
349
382
  const importPaths = extractImportPaths(tfContent);
@@ -383,9 +416,11 @@ export class ImpactAnalyzer {
383
416
  async detectBreaking(changedFiles, diffs) {
384
417
  try {
385
418
  const breaking = [];
419
+ const renameMap = new Map();
386
420
  for (let i = 0; i < changedFiles.length; i++) {
387
421
  const file = changedFiles[i];
388
- const diff = diffs?.[i] ?? (await gitDiff(this.projectPath, file));
422
+ const resolvedFile = resolveProjectPath(this.projectPath, file);
423
+ const diff = diffs?.[i] ?? (await gitDiff(this.projectPath, resolvedFile));
389
424
  if (!diff)
390
425
  continue;
391
426
  const lines = diff.split("\n");
@@ -471,6 +506,7 @@ export class ImpactAnalyzer {
471
506
  if (removedExports.length > 0 && removedExports.length === addedExports.length) {
472
507
  for (let j = 0; j < removedExports.length; j++) {
473
508
  if (removedExports[j] !== addedExports[j]) {
509
+ renameMap.set(removedExports[j], addedExports[j]);
474
510
  // Already reported as removed? Skip duplication
475
511
  if (!breaking.some((b) => b.description.includes(removedExports[j]))) {
476
512
  breaking.push({
@@ -483,6 +519,53 @@ export class ImpactAnalyzer {
483
519
  }
484
520
  }
485
521
  }
522
+ if (renameMap.size > 0) {
523
+ const renamedOldNames = new Set(renameMap.keys());
524
+ for (let k = breaking.length - 1; k >= 0; k--) {
525
+ const item = breaking[k];
526
+ if (item.file === file &&
527
+ item.severity === "critical") {
528
+ const m = item.description.match(/^Removed export "(.+)"$/);
529
+ if (m && renamedOldNames.has(m[1])) {
530
+ breaking.splice(k, 1);
531
+ }
532
+ }
533
+ }
534
+ }
535
+ }
536
+ // ─── rename impact propagation ───
537
+ if (renameMap.size > 0) {
538
+ const graph = await this.getGraph();
539
+ for (const [oldName, newName] of renameMap.entries()) {
540
+ for (const [usageFile, usages] of graph.symbolUsage.entries()) {
541
+ if (!usages.includes(oldName))
542
+ continue;
543
+ if (!breaking.some((b) => b.file === usageFile &&
544
+ b.description === `Symbol "${oldName}" renamed to "${newName}"`)) {
545
+ breaking.push({
546
+ file: usageFile,
547
+ description: `Symbol "${oldName}" renamed to "${newName}"`,
548
+ severity: "high",
549
+ suggestion: `Update import or usage to "${newName}".`,
550
+ });
551
+ }
552
+ }
553
+ }
554
+ for (const [file, calls] of graph.callGraph.entries()) {
555
+ for (const [oldName, newName] of renameMap.entries()) {
556
+ if (!calls.includes(oldName))
557
+ continue;
558
+ if (!breaking.some((b) => b.file === file &&
559
+ b.description === `Call to renamed function "${oldName}"`)) {
560
+ breaking.push({
561
+ file,
562
+ description: `Call to renamed function "${oldName}"`,
563
+ severity: "high",
564
+ suggestion: `Update call to "${newName}".`,
565
+ });
566
+ }
567
+ }
568
+ }
486
569
  }
487
570
  return breaking;
488
571
  }
@@ -558,12 +641,27 @@ export class ImpactAnalyzer {
558
641
  }
559
642
  async collectAffectedAPIs(changedFiles) {
560
643
  const apis = [];
644
+ const graph = await this.getGraph();
561
645
  for (const file of changedFiles) {
562
- const content = await readFileSafe(file);
646
+ const resolvedFile = resolveProjectPath(this.projectPath, file);
647
+ const content = graph.fileContents.get(resolvedFile) ?? await readFileSafe(resolvedFile);
563
648
  if (!content)
564
649
  continue;
565
650
  const fileAPIs = extractAPIs(content, file);
566
651
  apis.push(...fileAPIs);
652
+ const endpoints = graph.apiEndpoints.get(resolvedFile) ?? [];
653
+ for (const ep of endpoints) {
654
+ if (!ep)
655
+ continue;
656
+ if (!apis.some((a) => a.endpoint === ep && a.file === file)) {
657
+ apis.push({
658
+ endpoint: ep,
659
+ functionName: ep,
660
+ file,
661
+ changeType: "behavior",
662
+ });
663
+ }
664
+ }
567
665
  // Also extract exported functions as potential API surface
568
666
  const exports = extractExports(content);
569
667
  for (const exp of exports) {
@@ -578,6 +676,12 @@ export class ImpactAnalyzer {
578
676
  }
579
677
  return apis;
580
678
  }
679
+ async getGraph() {
680
+ if (!this.symbolGraph) {
681
+ this.symbolGraph = await SymbolGraph.build(this.projectPath);
682
+ }
683
+ return this.symbolGraph;
684
+ }
581
685
  buildSummary(changedFiles, affectedFiles, affectedTests, breakingChanges, riskLevel) {
582
686
  const parts = [];
583
687
  parts.push(`${changedFiles.length} file(s) changed, affecting ${affectedFiles.length} other file(s).`);
@@ -592,7 +696,126 @@ export class ImpactAnalyzer {
592
696
  parts.push(`Risk level: ${riskLevel}.`);
593
697
  return parts.join(" ");
594
698
  }
595
- buildSuggestedActions(affectedTests, breakingChanges, riskLevel) {
699
+ async detectDeadCode(changedFiles) {
700
+ const graph = await this.getGraph();
701
+ const out = [];
702
+ for (const file of changedFiles) {
703
+ const resolvedFile = resolveProjectPath(this.projectPath, file);
704
+ const exports = graph.exportsByFile.get(resolvedFile) ?? [];
705
+ const typeExports = graph.typeExportsByFile.get(resolvedFile) ?? [];
706
+ for (const symbol of exports) {
707
+ let used = false;
708
+ for (const [otherFile, usages] of graph.symbolUsage.entries()) {
709
+ if (otherFile === resolvedFile)
710
+ continue;
711
+ if (usages.includes(symbol)) {
712
+ used = true;
713
+ break;
714
+ }
715
+ }
716
+ if (!used) {
717
+ out.push({
718
+ file,
719
+ symbol,
720
+ kind: "export",
721
+ confidence: 0.75,
722
+ });
723
+ }
724
+ }
725
+ for (const symbol of typeExports) {
726
+ let used = false;
727
+ for (const [otherFile, usages] of graph.symbolUsage.entries()) {
728
+ if (otherFile === resolvedFile)
729
+ continue;
730
+ if (usages.includes(symbol)) {
731
+ used = true;
732
+ break;
733
+ }
734
+ }
735
+ if (!used) {
736
+ out.push({
737
+ file,
738
+ symbol,
739
+ kind: "type_export",
740
+ confidence: 0.7,
741
+ });
742
+ }
743
+ }
744
+ }
745
+ return out;
746
+ }
747
+ async inferTestCoverage(changedFiles) {
748
+ const tests = await this.suggestTests(changedFiles);
749
+ const out = [];
750
+ for (const file of changedFiles) {
751
+ const changedName = fileNameWithoutExt(file);
752
+ const direct = tests.some((t) => basename(t.path) === `${changedName}.test.ts` ||
753
+ basename(t.path) === `${changedName}.test.tsx` ||
754
+ basename(t.path) === `${changedName}.spec.ts` ||
755
+ basename(t.path) === `${changedName}.spec.tsx`);
756
+ const importerTests = tests.some((t) => t.reason.includes("imports changed module"));
757
+ let inferredCoverage = "unknown";
758
+ let reason = "No clear signals.";
759
+ if (direct) {
760
+ inferredCoverage = "high";
761
+ reason = "Direct test file exists.";
762
+ }
763
+ else if (importerTests) {
764
+ inferredCoverage = "medium";
765
+ reason = "Importer-based tests exist.";
766
+ }
767
+ else {
768
+ inferredCoverage = "low";
769
+ reason = "No direct or importer-linked tests found.";
770
+ }
771
+ out.push({
772
+ file,
773
+ hasDirectTest: direct,
774
+ hasImporterTests: importerTests,
775
+ inferredCoverage,
776
+ reason,
777
+ });
778
+ }
779
+ return out;
780
+ }
781
+ async buildSafeRefactorPlan(changedFiles) {
782
+ const graph = await this.getGraph();
783
+ const steps = [];
784
+ let step = 1;
785
+ for (const file of changedFiles) {
786
+ const resolvedFile = resolveProjectPath(this.projectPath, file);
787
+ const moduleKey = basename(resolvedFile).replace(/\.[jt]sx?$/, "");
788
+ const importers = [...(graph.reverseImports.get(moduleKey) ?? new Set())];
789
+ const exportedSymbols = graph.exportsByFile.get(resolvedFile) ?? [];
790
+ steps.push({
791
+ step: step++,
792
+ action: `Stabilize exports in ${file} with compatibility alias if needed.`,
793
+ files: [file],
794
+ risk: "low",
795
+ });
796
+ if (importers.length > 0) {
797
+ steps.push({
798
+ step: step++,
799
+ action: `Update importer references for ${file}.`,
800
+ files: importers,
801
+ risk: "medium",
802
+ });
803
+ }
804
+ const callSites = [...graph.callGraph.entries()]
805
+ .filter(([, calls]) => exportedSymbols.some((symbol) => calls.includes(symbol)))
806
+ .map(([callFile]) => callFile);
807
+ if (callSites.length > 0) {
808
+ steps.push({
809
+ step: step++,
810
+ action: `Review call sites potentially affected by refactor in ${file}.`,
811
+ files: callSites,
812
+ risk: "high",
813
+ });
814
+ }
815
+ }
816
+ return steps;
817
+ }
818
+ buildSuggestedActions(affectedTests, breakingChanges, riskLevel, deadCodeCandidates = [], testCoverage = []) {
596
819
  const actions = [];
597
820
  const mustRun = affectedTests.filter((t) => t.priority === "must_run");
598
821
  if (mustRun.length > 0) {
@@ -603,6 +826,12 @@ export class ImpactAnalyzer {
603
826
  actions.push(bc.suggestion);
604
827
  }
605
828
  }
829
+ if (deadCodeCandidates.length > 0) {
830
+ actions.push(`Review ${deadCodeCandidates.length} dead code candidate(s) before deleting exports.`);
831
+ }
832
+ if (testCoverage.some((t) => t.inferredCoverage === "low")) {
833
+ actions.push("Add tests for changed modules with low inferred coverage.");
834
+ }
606
835
  if (riskLevel === "high" || riskLevel === "critical") {
607
836
  actions.push("Request a thorough code review before merging.");
608
837
  }
@@ -612,4 +841,137 @@ export class ImpactAnalyzer {
612
841
  return actions;
613
842
  }
614
843
  }
844
+ // ─── Symbol Graph Cache ───
845
+ class SymbolGraph {
846
+ fileContents = new Map();
847
+ exportsByFile = new Map();
848
+ typeExportsByFile = new Map();
849
+ importsByFile = new Map();
850
+ reverseImports = new Map();
851
+ apiEndpoints = new Map();
852
+ symbolUsage = new Map();
853
+ callGraph = new Map();
854
+ /**
855
+ * Incrementally update graph for changed files.
856
+ */
857
+ async update(files) {
858
+ for (const file of files) {
859
+ const content = await readFileSafe(file);
860
+ if (!content) {
861
+ this.removeFile(file);
862
+ continue;
863
+ }
864
+ const prevImports = this.importsByFile.get(file) ?? [];
865
+ // remove previous reverse edges
866
+ for (const imp of prevImports) {
867
+ const base = basename(imp).replace(/\.[jt]sx?$/, "");
868
+ const set = this.reverseImports.get(base);
869
+ if (set) {
870
+ set.delete(file);
871
+ if (set.size === 0)
872
+ this.reverseImports.delete(base);
873
+ }
874
+ }
875
+ const exports = extractExports(content);
876
+ const typeExports = extractTypeExports(content);
877
+ const imports = extractImportPaths(content);
878
+ const apis = extractAPIs(content, file);
879
+ const symbols = extractSymbolUsage(content);
880
+ const calls = extractFunctionCalls(content);
881
+ this.fileContents.set(file, content);
882
+ this.exportsByFile.set(file, exports);
883
+ this.typeExportsByFile.set(file, typeExports);
884
+ this.importsByFile.set(file, imports);
885
+ this.symbolUsage.set(file, symbols);
886
+ this.callGraph.set(file, calls);
887
+ this.apiEndpoints.set(file, apis.map((a) => a.endpoint).filter((ep) => Boolean(ep)));
888
+ for (const imp of imports) {
889
+ const base = basename(imp).replace(/\.[jt]sx?$/, "");
890
+ if (!this.reverseImports.has(base)) {
891
+ this.reverseImports.set(base, new Set());
892
+ }
893
+ this.reverseImports.get(base).add(file);
894
+ }
895
+ }
896
+ }
897
+ removeFile(file) {
898
+ const prevImports = this.importsByFile.get(file) ?? [];
899
+ for (const imp of prevImports) {
900
+ const base = basename(imp).replace(/\.[jt]sx?$/, "");
901
+ const set = this.reverseImports.get(base);
902
+ if (set) {
903
+ set.delete(file);
904
+ if (set.size === 0)
905
+ this.reverseImports.delete(base);
906
+ }
907
+ }
908
+ this.fileContents.delete(file);
909
+ this.exportsByFile.delete(file);
910
+ this.typeExportsByFile.delete(file);
911
+ this.importsByFile.delete(file);
912
+ this.apiEndpoints.delete(file);
913
+ this.symbolUsage.delete(file);
914
+ this.callGraph.delete(file);
915
+ }
916
+ static async build(projectPath) {
917
+ const graph = new SymbolGraph();
918
+ const files = await walkDir(projectPath);
919
+ const contents = await Promise.all(files.map(async (f) => [f, await readFileSafe(f)]));
920
+ for (const [file, content] of contents) {
921
+ if (!content)
922
+ continue;
923
+ graph.fileContents.set(file, content);
924
+ const exports = extractExports(content);
925
+ const typeExports = extractTypeExports(content);
926
+ const imports = extractImportPaths(content);
927
+ const apis = extractAPIs(content, file);
928
+ const symbols = extractSymbolUsage(content);
929
+ const calls = extractFunctionCalls(content);
930
+ graph.exportsByFile.set(file, exports);
931
+ graph.typeExportsByFile.set(file, typeExports);
932
+ graph.importsByFile.set(file, imports);
933
+ graph.symbolUsage.set(file, symbols);
934
+ graph.callGraph.set(file, calls);
935
+ graph.apiEndpoints.set(file, apis.map((a) => a.endpoint).filter((ep) => Boolean(ep)));
936
+ for (const imp of imports) {
937
+ const base = basename(imp).replace(/\.[jt]sx?$/, "");
938
+ if (!graph.reverseImports.has(base)) {
939
+ graph.reverseImports.set(base, new Set());
940
+ }
941
+ graph.reverseImports.get(base).add(file);
942
+ }
943
+ }
944
+ return graph;
945
+ }
946
+ detectCycles() {
947
+ const cycles = [];
948
+ const visited = new Set();
949
+ const stack = new Set();
950
+ const visit = (node, path) => {
951
+ if (stack.has(node)) {
952
+ const cycleStart = path.indexOf(node);
953
+ cycles.push(path.slice(cycleStart));
954
+ return;
955
+ }
956
+ if (visited.has(node))
957
+ return;
958
+ visited.add(node);
959
+ stack.add(node);
960
+ const imports = this.importsByFile.get(node) ?? [];
961
+ for (const imp of imports) {
962
+ const base = basename(imp).replace(/\.[jt]sx?$/, "");
963
+ for (const [file] of this.fileContents) {
964
+ if (basename(file).replace(/\.[jt]sx?$/, "") === base) {
965
+ visit(file, [...path, file]);
966
+ }
967
+ }
968
+ }
969
+ stack.delete(node);
970
+ };
971
+ for (const file of this.fileContents.keys()) {
972
+ visit(file, [file]);
973
+ }
974
+ return cycles;
975
+ }
976
+ }
615
977
  //# sourceMappingURL=impact-analyzer.js.map