@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.
- package/dist/agent-logger.d.ts +1 -1
- package/dist/agent-logger.d.ts.map +1 -1
- package/dist/agent-logger.js +17 -15
- package/dist/agent-logger.js.map +1 -1
- package/dist/agent-loop.d.ts +31 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +514 -98
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent-modes.d.ts.map +1 -1
- package/dist/agent-modes.js +5 -0
- package/dist/agent-modes.js.map +1 -1
- package/dist/async-completion-queue.d.ts +2 -0
- package/dist/async-completion-queue.d.ts.map +1 -1
- package/dist/async-completion-queue.js +14 -0
- package/dist/async-completion-queue.js.map +1 -1
- package/dist/auto-fix.d.ts.map +1 -1
- package/dist/auto-fix.js +12 -1
- package/dist/auto-fix.js.map +1 -1
- package/dist/benchmark-runner.d.ts.map +1 -1
- package/dist/benchmark-runner.js +5 -1
- package/dist/benchmark-runner.js.map +1 -1
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -1
- package/dist/context-manager.d.ts +25 -0
- package/dist/context-manager.d.ts.map +1 -1
- package/dist/context-manager.js +132 -5
- package/dist/context-manager.js.map +1 -1
- package/dist/continuation-engine.d.ts.map +1 -1
- package/dist/continuation-engine.js +8 -7
- package/dist/continuation-engine.js.map +1 -1
- package/dist/continuous-reflection.d.ts.map +1 -1
- package/dist/continuous-reflection.js +22 -12
- package/dist/continuous-reflection.js.map +1 -1
- package/dist/cost-optimizer.js +1 -1
- package/dist/cost-optimizer.js.map +1 -1
- package/dist/cross-file-refactor.d.ts.map +1 -1
- package/dist/cross-file-refactor.js +7 -2
- package/dist/cross-file-refactor.js.map +1 -1
- package/dist/dag-orchestrator.d.ts +10 -1
- package/dist/dag-orchestrator.d.ts.map +1 -1
- package/dist/dag-orchestrator.js +101 -6
- package/dist/dag-orchestrator.js.map +1 -1
- package/dist/debate-orchestrator.d.ts +1 -0
- package/dist/debate-orchestrator.d.ts.map +1 -1
- package/dist/debate-orchestrator.js +27 -15
- package/dist/debate-orchestrator.js.map +1 -1
- package/dist/dependency-analyzer.d.ts.map +1 -1
- package/dist/dependency-analyzer.js +19 -1
- package/dist/dependency-analyzer.js.map +1 -1
- package/dist/dynamic-role-generator.d.ts.map +1 -1
- package/dist/dynamic-role-generator.js +6 -3
- package/dist/dynamic-role-generator.js.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/event-bus.d.ts.map +1 -1
- package/dist/event-bus.js +4 -3
- package/dist/event-bus.js.map +1 -1
- package/dist/execution-engine.d.ts +39 -1
- package/dist/execution-engine.d.ts.map +1 -1
- package/dist/execution-engine.js +453 -83
- package/dist/execution-engine.js.map +1 -1
- package/dist/failure-recovery.d.ts.map +1 -1
- package/dist/failure-recovery.js +14 -3
- package/dist/failure-recovery.js.map +1 -1
- package/dist/git-intelligence.d.ts.map +1 -1
- package/dist/git-intelligence.js +16 -11
- package/dist/git-intelligence.js.map +1 -1
- package/dist/governor.d.ts +8 -0
- package/dist/governor.d.ts.map +1 -1
- package/dist/governor.js +19 -1
- package/dist/governor.js.map +1 -1
- package/dist/hierarchical-planner.d.ts +3 -0
- package/dist/hierarchical-planner.d.ts.map +1 -1
- package/dist/hierarchical-planner.js +32 -2
- package/dist/hierarchical-planner.js.map +1 -1
- package/dist/impact-analyzer.d.ts +27 -0
- package/dist/impact-analyzer.d.ts.map +1 -1
- package/dist/impact-analyzer.js +415 -53
- package/dist/impact-analyzer.js.map +1 -1
- package/dist/intent-inference.d.ts.map +1 -1
- package/dist/intent-inference.js +20 -24
- package/dist/intent-inference.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +5 -3
- package/dist/kernel.js.map +1 -1
- package/dist/language-detector.d.ts +19 -0
- package/dist/language-detector.d.ts.map +1 -0
- package/dist/language-detector.js +482 -0
- package/dist/language-detector.js.map +1 -0
- package/dist/language-support.d.ts.map +1 -1
- package/dist/language-support.js +5 -9
- package/dist/language-support.js.map +1 -1
- package/dist/llm-client.d.ts +21 -8
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +125 -21
- package/dist/llm-client.js.map +1 -1
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +9 -1
- package/dist/mcp-client.js.map +1 -1
- package/dist/memory-manager.d.ts +13 -8
- package/dist/memory-manager.d.ts.map +1 -1
- package/dist/memory-manager.js +125 -32
- package/dist/memory-manager.js.map +1 -1
- package/dist/memory-updater.d.ts.map +1 -1
- package/dist/memory-updater.js +5 -4
- package/dist/memory-updater.js.map +1 -1
- package/dist/memory.d.ts +6 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +32 -4
- package/dist/memory.js.map +1 -1
- package/dist/parallel-executor.d.ts +7 -0
- package/dist/parallel-executor.d.ts.map +1 -1
- package/dist/parallel-executor.js +28 -0
- package/dist/parallel-executor.js.map +1 -1
- package/dist/perf-optimizer.d.ts.map +1 -1
- package/dist/perf-optimizer.js +18 -3
- package/dist/perf-optimizer.js.map +1 -1
- package/dist/persona.d.ts.map +1 -1
- package/dist/persona.js +8 -3
- package/dist/persona.js.map +1 -1
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +5 -3
- package/dist/planner.js.map +1 -1
- package/dist/plugin-auto-loader.d.ts.map +1 -1
- package/dist/plugin-auto-loader.js +4 -1
- package/dist/plugin-auto-loader.js.map +1 -1
- package/dist/plugin-registry.d.ts +4 -0
- package/dist/plugin-registry.d.ts.map +1 -1
- package/dist/plugin-registry.js +6 -0
- package/dist/plugin-registry.js.map +1 -1
- package/dist/plugin-validator.d.ts.map +1 -1
- package/dist/plugin-validator.js +10 -1
- package/dist/plugin-validator.js.map +1 -1
- package/dist/reasoning-aggregator.d.ts +35 -0
- package/dist/reasoning-aggregator.d.ts.map +1 -0
- package/dist/reasoning-aggregator.js +102 -0
- package/dist/reasoning-aggregator.js.map +1 -0
- package/dist/reasoning-tree.d.ts +23 -0
- package/dist/reasoning-tree.d.ts.map +1 -0
- package/dist/reasoning-tree.js +44 -0
- package/dist/reasoning-tree.js.map +1 -0
- package/dist/session-persistence.d.ts +8 -4
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +22 -7
- package/dist/session-persistence.js.map +1 -1
- package/dist/skill-learner.d.ts.map +1 -1
- package/dist/skill-learner.js +4 -2
- package/dist/skill-learner.js.map +1 -1
- package/dist/skill-loader.d.ts +4 -0
- package/dist/skill-loader.d.ts.map +1 -1
- package/dist/skill-loader.js +6 -0
- package/dist/skill-loader.js.map +1 -1
- package/dist/speculative-executor.d.ts +22 -0
- package/dist/speculative-executor.d.ts.map +1 -1
- package/dist/speculative-executor.js +90 -45
- package/dist/speculative-executor.js.map +1 -1
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +4 -2
- package/dist/state-machine.js.map +1 -1
- package/dist/sub-agent-prompts.d.ts +5 -29
- package/dist/sub-agent-prompts.d.ts.map +1 -1
- package/dist/sub-agent-prompts.js +231 -134
- package/dist/sub-agent-prompts.js.map +1 -1
- package/dist/sub-agent.d.ts +19 -0
- package/dist/sub-agent.d.ts.map +1 -1
- package/dist/sub-agent.js +135 -11
- package/dist/sub-agent.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +45 -0
- package/dist/system-prompt.js.map +1 -1
- package/dist/task-classifier.js +1 -1
- package/dist/task-classifier.js.map +1 -1
- package/dist/types.d.ts +67 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-index.d.ts +14 -0
- package/dist/vector-index.d.ts.map +1 -1
- package/dist/vector-index.js +84 -16
- package/dist/vector-index.js.map +1 -1
- package/dist/workspace-lock.d.ts +5 -0
- package/dist/workspace-lock.d.ts.map +1 -0
- package/dist/workspace-lock.js +16 -0
- package/dist/workspace-lock.js.map +1 -0
- package/package.json +1 -1
package/dist/impact-analyzer.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
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 =
|
|
191
|
-
const
|
|
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
|
-
|
|
197
|
-
for (const
|
|
198
|
-
if (
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
255
|
-
if (seen.has(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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|