entkapp 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.scaffold-ignore +22 -0
- package/LICENSE +211 -0
- package/NOTICE +13 -0
- package/README.md +33 -0
- package/bin/cli.js +174 -0
- package/entkapp/config.json +42 -0
- package/entkapp/plugins/README.md +19 -0
- package/index.js +2254 -0
- package/logo.png +0 -0
- package/package.json +96 -0
- package/src/EngineContext.js +185 -0
- package/src/api/HeadlessAPI.js +376 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/ast/ASTAnalyzer.js +492 -0
- package/src/ast/BarrelParser.js +221 -0
- package/src/ast/DeadCodeDetector.js +73 -0
- package/src/ast/MagicDetector.js +203 -0
- package/src/ast/OxcAnalyzer.js +337 -0
- package/src/ast/SecretScanner.js +304 -0
- package/src/healing/GitSandbox.js +82 -0
- package/src/healing/SelfHealer.js +52 -0
- package/src/index.js +723 -0
- package/src/performance/GraphCache.js +87 -0
- package/src/performance/SupplyChainGuard.js +92 -0
- package/src/performance/WorkerPool.js +109 -0
- package/src/plugins/BasePlugin.js +71 -0
- package/src/plugins/KnipAdapter.js +106 -0
- package/src/plugins/PluginRegistry.js +197 -0
- package/src/plugins/ecosystems/BackendServices.js +61 -0
- package/src/plugins/ecosystems/GenericPlugins.js +64 -0
- package/src/plugins/ecosystems/ModernFrameworks.js +159 -0
- package/src/plugins/ecosystems/MorePlugins.js +184 -0
- package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
- package/src/plugins/ecosystems/PluginLoader.js +20 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
- package/src/refractor/ImpactAnalyzer.js +92 -0
- package/src/refractor/SourceRewriter.js +86 -0
- package/src/refractor/TransactionManager.js +138 -0
- package/src/refractor/TypeIntegrity.js +75 -0
- package/src/resolution/CircularDetector.js +91 -0
- package/src/resolution/ConfigLoader.js +87 -0
- package/src/resolution/DepencyResolver.js +133 -0
- package/src/resolution/DependencyProfiler.js +268 -0
- package/src/resolution/PathMapper.js +125 -0
- package/src/resolution/WorkSpaceGraph.js +474 -0
- package/tsconfig.json +26 -0
package/logo.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"name": "entkapp",
|
|
4
|
+
"version": "4.1.0",
|
|
5
|
+
"description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"entkapp": "./bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node bin/cli.js",
|
|
13
|
+
"entkapp:run": "npx entkapp --fix",
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 0",
|
|
15
|
+
"test:stability": "npm run test",
|
|
16
|
+
"docs:dev": "vitepress dev docs",
|
|
17
|
+
"docs:build": "vitepress build docs && cp docs/sitemap.xml docs/.vitepress/dist/ && cp docs/robots.txt docs/.vitepress/dist && cp logo.png docs/.vitepress/dist",
|
|
18
|
+
"docs:preview": "vitepress preview docs"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"analysis",
|
|
22
|
+
"analyze",
|
|
23
|
+
"ast",
|
|
24
|
+
"automation",
|
|
25
|
+
"cli",
|
|
26
|
+
"gui",
|
|
27
|
+
"class",
|
|
28
|
+
"dead-code",
|
|
29
|
+
"dependencies",
|
|
30
|
+
"dependency-analysis",
|
|
31
|
+
"dependency-audit",
|
|
32
|
+
"detect",
|
|
33
|
+
"devDependencies",
|
|
34
|
+
"duplicate",
|
|
35
|
+
"entropy",
|
|
36
|
+
"enum",
|
|
37
|
+
"export",
|
|
38
|
+
"files",
|
|
39
|
+
"find",
|
|
40
|
+
"ghost-dependencies",
|
|
41
|
+
"javascript",
|
|
42
|
+
"maintenance",
|
|
43
|
+
"members",
|
|
44
|
+
"missing",
|
|
45
|
+
"missing-imports",
|
|
46
|
+
"monorepo",
|
|
47
|
+
"namespace",
|
|
48
|
+
"oxc",
|
|
49
|
+
"package.json",
|
|
50
|
+
"packages",
|
|
51
|
+
"scan",
|
|
52
|
+
"security",
|
|
53
|
+
"secret-scanner",
|
|
54
|
+
"secrets",
|
|
55
|
+
"hardcoded-secrets",
|
|
56
|
+
"credentials",
|
|
57
|
+
"audit",
|
|
58
|
+
"structural-healing",
|
|
59
|
+
"setup",
|
|
60
|
+
"type",
|
|
61
|
+
"types",
|
|
62
|
+
"typescript",
|
|
63
|
+
"unreferenced",
|
|
64
|
+
"unresolved",
|
|
65
|
+
"unused",
|
|
66
|
+
"unused-packages",
|
|
67
|
+
"workspace"
|
|
68
|
+
],
|
|
69
|
+
"author": "DreamLongYT",
|
|
70
|
+
"license": "Apache-2.0",
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "git+https://github.com/DreamLongYT/entkapp.git"
|
|
74
|
+
},
|
|
75
|
+
"bugs": {
|
|
76
|
+
"url": "https://github.com/DreamLongYT/entkapp/issues"
|
|
77
|
+
},
|
|
78
|
+
"homepage": "https://dreamlongyt.github.io/entkapp/",
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"ansis": "^3.0.0",
|
|
81
|
+
"commander": "^12.0.0",
|
|
82
|
+
"enhanced-resolve": "^5.16.0",
|
|
83
|
+
"execa": "^8.0.1",
|
|
84
|
+
"oxc-parser": "^0.135.0",
|
|
85
|
+
"typescript": "^5.0.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@types/node": "^25.9.3",
|
|
89
|
+
"knip": "^6.17.1",
|
|
90
|
+
"vitepress": "^1.6.4",
|
|
91
|
+
"vue": ">=3.5.0"
|
|
92
|
+
},
|
|
93
|
+
"engines": {
|
|
94
|
+
"node": ">=18.0.0"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Core In-Memory Graph Node Representation.
|
|
5
|
+
* Tracks structural metadata, dependencies, and usage metrics for a single file.
|
|
6
|
+
*/
|
|
7
|
+
export class GraphNode {
|
|
8
|
+
constructor(filePath) {
|
|
9
|
+
this.filePath = filePath;
|
|
10
|
+
this.incomingEdges = new Set(); // Files that import THIS file
|
|
11
|
+
this.outgoingEdges = new Set(); // Files that THIS file imports
|
|
12
|
+
|
|
13
|
+
this.explicitImports = new Set(); // Raw import strings
|
|
14
|
+
this.importedSymbols = new Set(); // "file:symbol" or "file:*"
|
|
15
|
+
this.internalExports = new Map(); // symbol -> { type, start, end, ... }
|
|
16
|
+
|
|
17
|
+
this.externalPackageUsage = new Set();
|
|
18
|
+
this.instantiatedIdentifiers = new Set();
|
|
19
|
+
this.propertyAccessChains = new Set();
|
|
20
|
+
this.rawStringReferences = new Set();
|
|
21
|
+
this.jsxComponents = new Set();
|
|
22
|
+
this.jsxProps = new Set();
|
|
23
|
+
this.decorators = new Set();
|
|
24
|
+
this.dynamicImports = new Set();
|
|
25
|
+
this.calculatedDynamicImports = [];
|
|
26
|
+
|
|
27
|
+
this.isEntry = false;
|
|
28
|
+
this.isLibraryEntry = false;
|
|
29
|
+
this.isFrameworkComponent = false;
|
|
30
|
+
this.symbolSourceLocations = new Map(); // symbol -> { line, column }
|
|
31
|
+
this.localSuppressedRules = new Set();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isSymbolReferencedExternally(symbolName, projectGraph) {
|
|
35
|
+
if (this.isLibraryEntry) return true;
|
|
36
|
+
|
|
37
|
+
// --- NEW: Self-reference check (Fix for SecretSeverity bug) ---
|
|
38
|
+
// If the symbol is used within the same file, it is NOT dead.
|
|
39
|
+
if (this.instantiatedIdentifiers.has(symbolName)) return true;
|
|
40
|
+
|
|
41
|
+
// Check property access in the same file
|
|
42
|
+
for (const accessChain of this.propertyAccessChains) {
|
|
43
|
+
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// --------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
for (const parentPath of this.incomingEdges) {
|
|
50
|
+
const parentNode = projectGraph.get(parentPath);
|
|
51
|
+
if (!parentNode) continue;
|
|
52
|
+
|
|
53
|
+
// Strategy 1: Check absolute path based tokens
|
|
54
|
+
const absoluteImportKey = `${this.filePath}:${symbolName}`;
|
|
55
|
+
const absoluteStarKey = `${this.filePath}:*`;
|
|
56
|
+
|
|
57
|
+
if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Strategy 2: Check relative path based tokens
|
|
62
|
+
const relativePath = path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/');
|
|
63
|
+
const relativePathNoExt = relativePath.replace(/\.(js|ts|tsx|jsx)$/, '');
|
|
64
|
+
|
|
65
|
+
const importKey = `${relativePath}:${symbolName}`;
|
|
66
|
+
const importKeyAlt = `${relativePathNoExt}:${symbolName}`;
|
|
67
|
+
const starKey = `${relativePath}:*`;
|
|
68
|
+
const starKeyAlt = `${relativePathNoExt}:*`;
|
|
69
|
+
|
|
70
|
+
if (parentNode.importedSymbols.has(importKey) || parentNode.importedSymbols.has(importKeyAlt) ||
|
|
71
|
+
parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parentNode.instantiatedIdentifiers.has(symbolName)) return true;
|
|
76
|
+
|
|
77
|
+
for (const accessChain of parentNode.propertyAccessChains) {
|
|
78
|
+
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (parentNode.rawStringReferences.has(symbolName)) return true;
|
|
84
|
+
if (parentNode.jsxComponents.has(symbolName)) return true;
|
|
85
|
+
|
|
86
|
+
for (const jsxProp of parentNode.jsxProps) {
|
|
87
|
+
if (jsxProp.endsWith(`:${symbolName}`)) return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (parentNode.decorators.has(symbolName)) return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class EngineContext {
|
|
98
|
+
constructor(cwd) {
|
|
99
|
+
// FIX: Ensure cwd is always defined, fallback to process.cwd()
|
|
100
|
+
this.cwd = cwd || process.cwd();
|
|
101
|
+
this.projectGraph = new Map(); // Path -> GraphNode
|
|
102
|
+
this.usedExternalPackages = new Set();
|
|
103
|
+
this.unimportedUsedPackages = new Set(); // NEW: For "Unimported but used"
|
|
104
|
+
this.importedUnusedPackages = new Set(); // NEW: For "Imported but unused"
|
|
105
|
+
this.unusedBinaries = new Set(); // NEW: For "Unused Binaries"
|
|
106
|
+
this.manifestDependencies = new Map(); // NEW: Store dependencies from package.json
|
|
107
|
+
|
|
108
|
+
this.isWorkspaceEnabled = false;
|
|
109
|
+
this.monorepoPackageRoots = new Set();
|
|
110
|
+
this.verbose = false;
|
|
111
|
+
this.metrics = { totalFilesScanned: 0, cacheHits: 0, cacheMisses: 0 };
|
|
112
|
+
this.allSecretFindings = [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getOrCreateNode(filePath) {
|
|
116
|
+
if (!this.projectGraph.has(filePath)) {
|
|
117
|
+
this.projectGraph.set(filePath, new GraphNode(filePath));
|
|
118
|
+
}
|
|
119
|
+
return this.projectGraph.get(filePath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
generateSummaryReport() {
|
|
123
|
+
const report = {
|
|
124
|
+
orphanedFiles: [],
|
|
125
|
+
deadExports: [],
|
|
126
|
+
unusedDependencies: [],
|
|
127
|
+
unimportedUsedPackages: [], // NEW
|
|
128
|
+
importedUnusedPackages: [], // NEW
|
|
129
|
+
unusedBinaries: [] // NEW
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// 1. Files & Exports
|
|
133
|
+
for (const [filePath, node] of this.projectGraph.entries()) {
|
|
134
|
+
const isSuppressed = node.localSuppressedRules.has('*') || node.localSuppressedRules.has('unused-file');
|
|
135
|
+
|
|
136
|
+
if (node.incomingEdges.size === 0 && !node.isEntry && !node.isLibraryEntry && !node.isFrameworkComponent && !isSuppressed) {
|
|
137
|
+
report.orphanedFiles.push(path.relative(this.cwd, filePath));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const [symbol, meta] of node.internalExports.entries()) {
|
|
141
|
+
if (symbol === '*' || symbol === 'default' || node.localSuppressedRules.has('unused-export') || node.localSuppressedRules.has(`unused-export:${symbol}`)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!node.isSymbolReferencedExternally(symbol, this.projectGraph)) {
|
|
146
|
+
const loc = node.symbolSourceLocations.get(symbol) || { line: 0, column: 0 };
|
|
147
|
+
report.deadExports.push({
|
|
148
|
+
symbol,
|
|
149
|
+
file: path.relative(this.cwd, filePath),
|
|
150
|
+
line: loc.line
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. Dependency Analysis (Extended)
|
|
157
|
+
// Compare manifest dependencies with actually used packages
|
|
158
|
+
for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
|
|
159
|
+
const allDeps = [...(deps.dependencies || []), ...(deps.devDependencies || [])];
|
|
160
|
+
|
|
161
|
+
for (const dep of allDeps) {
|
|
162
|
+
// Skip @types packages and known safe packages
|
|
163
|
+
if (dep.startsWith('@types/') || dep === 'entkapp') {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if the dependency is actually used in the code
|
|
168
|
+
if (!this.usedExternalPackages.has(dep)) {
|
|
169
|
+
report.unusedDependencies.push({
|
|
170
|
+
package: dep,
|
|
171
|
+
type: deps.dependencies.includes(dep) ? 'dependency' : 'devDependency',
|
|
172
|
+
manifest: path.relative(this.cwd, manifestPath)
|
|
173
|
+
});
|
|
174
|
+
this.importedUnusedPackages.add(dep);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
report.unimportedUsedPackages = Array.from(this.unimportedUsedPackages);
|
|
180
|
+
report.importedUnusedPackages = Array.from(this.importedUnusedPackages);
|
|
181
|
+
report.unusedBinaries = Array.from(this.unusedBinaries);
|
|
182
|
+
|
|
183
|
+
return report;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Headless API for entkapp v4.1.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Provides a programmatic interface for integrating entkapp into
|
|
6
|
+
* custom workflows, CI/CD pipelines, and third-party tools.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Full control over analysis and refactoring operations
|
|
10
|
+
* - Event-driven architecture for real-time feedback
|
|
11
|
+
* - Streaming results for large codebases
|
|
12
|
+
* - Plugin SDK integration
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import EventEmitter from 'events';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { RefactoringEngine } from '../index.js';
|
|
18
|
+
|
|
19
|
+
export class HeadlessAPI extends EventEmitter {
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
super();
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.engine = null;
|
|
24
|
+
this.analysisResults = null;
|
|
25
|
+
this.isRunning = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the API with a project context
|
|
30
|
+
* @param {string} projectRoot - Root directory of the project
|
|
31
|
+
* @param {Object} config - Configuration options
|
|
32
|
+
* @returns {Promise<void>}
|
|
33
|
+
*/
|
|
34
|
+
async initialize(projectRoot, config = {}) {
|
|
35
|
+
try {
|
|
36
|
+
this.emit('initialize:start', { projectRoot });
|
|
37
|
+
|
|
38
|
+
const engineOptions = {
|
|
39
|
+
cwd: projectRoot,
|
|
40
|
+
autoFix: config.autoFix !== false,
|
|
41
|
+
skipConfirm: config.skipConfirm || false,
|
|
42
|
+
verbose: config.verbose || false,
|
|
43
|
+
...config
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.engine = new RefactoringEngine(engineOptions);
|
|
47
|
+
|
|
48
|
+
this.emit('initialize:complete', {
|
|
49
|
+
projectRoot,
|
|
50
|
+
config: this.engine.context
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.emit('initialize:error', { error });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Analyze the codebase without making changes
|
|
60
|
+
* @returns {Promise<Object>} Analysis results
|
|
61
|
+
*/
|
|
62
|
+
async analyze() {
|
|
63
|
+
if (!this.engine) {
|
|
64
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
this.isRunning = true;
|
|
69
|
+
this.emit('analysis:start');
|
|
70
|
+
|
|
71
|
+
// Initialize path mappings and workspace graph
|
|
72
|
+
await this.engine.pathMapper.loadMappings(this.engine.context.tsconfigFilename);
|
|
73
|
+
|
|
74
|
+
if (this.engine.context.isWorkspaceEnabled) {
|
|
75
|
+
this.emit('analysis:workspace-mapping-start');
|
|
76
|
+
await this.engine.workspaceGraph.initializeWorkspaceMesh();
|
|
77
|
+
this.emit('analysis:workspace-mapping-complete');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Load cache manifest
|
|
81
|
+
const cacheManifest = await this.engine.cacheManager.loadCacheManifest();
|
|
82
|
+
|
|
83
|
+
// Discover source files
|
|
84
|
+
this.emit('analysis:file-discovery-start');
|
|
85
|
+
const fileList = [];
|
|
86
|
+
await this.engine.discoverSourceFiles(this.engine.context.cwd, fileList);
|
|
87
|
+
this.engine.context.metrics.totalFilesScanned = fileList.length;
|
|
88
|
+
this.emit('analysis:file-discovery-complete', { fileCount: fileList.length });
|
|
89
|
+
|
|
90
|
+
// Identify framework ecosystems
|
|
91
|
+
this.emit('analysis:framework-detection-start');
|
|
92
|
+
const activeFrameworkEcosystems = await this.engine.magicDetector.identifyActiveProjectEcosystems(
|
|
93
|
+
this.engine.context.cwd
|
|
94
|
+
);
|
|
95
|
+
this.emit('analysis:framework-detection-complete', { ecosystems: activeFrameworkEcosystems });
|
|
96
|
+
|
|
97
|
+
// Process files
|
|
98
|
+
this.emit('analysis:file-processing-start');
|
|
99
|
+
const sourceCodeFilesList = [];
|
|
100
|
+
for (const file of fileList) {
|
|
101
|
+
if (file.endsWith('package.json')) {
|
|
102
|
+
await this.engine.auditManifestSupplyChain(file);
|
|
103
|
+
} else {
|
|
104
|
+
sourceCodeFilesList.push(file);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Initialize TypeScript program for AST analysis (required before processFile)
|
|
109
|
+
if (sourceCodeFilesList.length > 0) {
|
|
110
|
+
try {
|
|
111
|
+
this.engine.analyzer.initProgram(sourceCodeFilesList);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
if (this.engine.context.verbose) {
|
|
114
|
+
console.warn('Warning: Failed to initialize TypeScript program:', e.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parallel processing
|
|
120
|
+
let parallelParseCompleted = false;
|
|
121
|
+
if (sourceCodeFilesList.length > 10) {
|
|
122
|
+
parallelParseCompleted = await this.engine.workerPool.parallelAnalyzeCodebase(
|
|
123
|
+
sourceCodeFilesList,
|
|
124
|
+
this.engine
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Sequential processing
|
|
129
|
+
for (let i = 0; i < sourceCodeFilesList.length; i++) {
|
|
130
|
+
const filePath = sourceCodeFilesList[i];
|
|
131
|
+
const node = this.engine.context.getOrCreateNode(filePath);
|
|
132
|
+
const currentHash = await this.engine.cacheManager.computeHash(filePath);
|
|
133
|
+
node.contentHash = currentHash;
|
|
134
|
+
|
|
135
|
+
const isFileCached = cacheManifest[filePath] && cacheManifest[filePath].hash === currentHash;
|
|
136
|
+
|
|
137
|
+
if (isFileCached) {
|
|
138
|
+
this.engine.context.metrics.cacheHits++;
|
|
139
|
+
this.engine.hydrateNodeFromCache(node, cacheManifest[filePath]);
|
|
140
|
+
} else if (!parallelParseCompleted) {
|
|
141
|
+
this.engine.context.metrics.cacheMisses++;
|
|
142
|
+
await this.engine.analyzer.parseFile(filePath, "", node);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.engine.magicDetector.injectVirtualConsumerEdges(filePath, node, activeFrameworkEcosystems);
|
|
146
|
+
node.externalPackageUsage.forEach(pkg => this.engine.context.usedExternalPackages.add(pkg));
|
|
147
|
+
|
|
148
|
+
// Emit progress
|
|
149
|
+
if ((i + 1) % Math.ceil(sourceCodeFilesList.length / 10) === 0) {
|
|
150
|
+
this.emit('analysis:file-processing-progress', {
|
|
151
|
+
processed: i + 1,
|
|
152
|
+
total: sourceCodeFilesList.length,
|
|
153
|
+
percentage: Math.round(((i + 1) / sourceCodeFilesList.length) * 100)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Link dependency graph
|
|
159
|
+
this.emit('analysis:graph-linking-start');
|
|
160
|
+
await this.engine.linkDependencyGraph();
|
|
161
|
+
this.emit('analysis:graph-linking-complete');
|
|
162
|
+
|
|
163
|
+
// Generate summary
|
|
164
|
+
this.analysisResults = this.engine.context.generateSummaryReport();
|
|
165
|
+
this.emit('analysis:complete', this.analysisResults);
|
|
166
|
+
|
|
167
|
+
this.isRunning = false;
|
|
168
|
+
return this.analysisResults;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.isRunning = false;
|
|
171
|
+
this.emit('analysis:error', { error });
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get detailed impact analysis for a specific file or export
|
|
178
|
+
* @param {string} filePath - Path to the file
|
|
179
|
+
* @param {string} symbol - Optional: specific export symbol to analyze
|
|
180
|
+
* @returns {Promise<Object>} Impact analysis results
|
|
181
|
+
*/
|
|
182
|
+
async getImpactAnalysis(filePath, symbol = null) {
|
|
183
|
+
if (!this.engine) {
|
|
184
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const node = this.engine.context.projectGraph.get(filePath);
|
|
189
|
+
if (!node) {
|
|
190
|
+
throw new Error(`File not found in analysis graph: ${filePath}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const impact = {
|
|
194
|
+
file: filePath,
|
|
195
|
+
symbol,
|
|
196
|
+
directDependents: Array.from(node.incomingEdges),
|
|
197
|
+
dependencies: Array.from(node.outgoingEdges),
|
|
198
|
+
internalExports: symbol
|
|
199
|
+
? [symbol]
|
|
200
|
+
: Array.from(node.internalExports.keys()),
|
|
201
|
+
externalPackages: Array.from(node.externalPackageUsage)
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (symbol) {
|
|
205
|
+
const safety = await this.engine.impactAnalyzer.verifyRefactorSafety(
|
|
206
|
+
filePath,
|
|
207
|
+
symbol,
|
|
208
|
+
this.engine.context.projectGraph
|
|
209
|
+
);
|
|
210
|
+
impact.refactorSafety = safety;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return impact;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this.emit('impact-analysis:error', { error, filePath, symbol });
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Apply refactoring changes with automatic rollback on test failure
|
|
222
|
+
* @param {Object} changes - Changes to apply
|
|
223
|
+
* @returns {Promise<Object>} Refactoring results
|
|
224
|
+
*/
|
|
225
|
+
async applyRefactoring(changes = {}) {
|
|
226
|
+
if (!this.engine) {
|
|
227
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!this.analysisResults) {
|
|
231
|
+
throw new Error('No analysis results available. Call analyze() first.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
this.isRunning = true;
|
|
236
|
+
this.emit('refactoring:start', changes);
|
|
237
|
+
|
|
238
|
+
const refactoringResults = {
|
|
239
|
+
filesDeleted: [],
|
|
240
|
+
exportsRemoved: [],
|
|
241
|
+
dependenciesRemoved: [],
|
|
242
|
+
errors: []
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await this.engine.selfHealer.runSelfHealingLifecycle(async () => {
|
|
246
|
+
// Delete dead files
|
|
247
|
+
const filesToDelete = changes.deleteDeadFiles !== false
|
|
248
|
+
? this.analysisResults.orphanedFiles
|
|
249
|
+
: [];
|
|
250
|
+
|
|
251
|
+
for (const relPath of filesToDelete) {
|
|
252
|
+
try {
|
|
253
|
+
const absPath = path.resolve(this.engine.context.cwd, relPath);
|
|
254
|
+
await this.engine.txManager.stageDeletion(absPath);
|
|
255
|
+
refactoringResults.filesDeleted.push(relPath);
|
|
256
|
+
this.emit('refactoring:file-deleted', { file: relPath });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
refactoringResults.errors.push({ type: 'file-deletion', file: relPath, error: error.message });
|
|
259
|
+
this.emit('refactoring:error', { type: 'file-deletion', file: relPath, error });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Remove unused exports
|
|
264
|
+
const exportsToRemove = changes.removeUnusedExports !== false
|
|
265
|
+
? this.analysisResults.deadExports
|
|
266
|
+
: [];
|
|
267
|
+
|
|
268
|
+
for (const unusedExport of exportsToRemove) {
|
|
269
|
+
try {
|
|
270
|
+
const absPath = path.resolve(this.engine.context.cwd, unusedExport.file);
|
|
271
|
+
const node = this.engine.context.projectGraph.get(absPath);
|
|
272
|
+
|
|
273
|
+
if (node) {
|
|
274
|
+
const safety = await this.engine.impactAnalyzer.verifyRefactorSafety(
|
|
275
|
+
absPath,
|
|
276
|
+
unusedExport.symbol,
|
|
277
|
+
this.engine.context.projectGraph
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (safety.isSafeToPrune) {
|
|
281
|
+
const nextText = await this.engine.sourceRewriter.stripNamedExportSignature(
|
|
282
|
+
absPath,
|
|
283
|
+
unusedExport.symbol,
|
|
284
|
+
node.internalExports.get(unusedExport.symbol)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await this.engine.txManager.stageWrite(absPath, nextText);
|
|
288
|
+
await this.engine.typeIntegrity.synchronizeDeclarationFile(absPath, unusedExport.symbol);
|
|
289
|
+
|
|
290
|
+
refactoringResults.exportsRemoved.push(unusedExport);
|
|
291
|
+
this.emit('refactoring:export-removed', unusedExport);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
refactoringResults.errors.push({
|
|
296
|
+
type: 'export-removal',
|
|
297
|
+
export: unusedExport.symbol,
|
|
298
|
+
file: unusedExport.file,
|
|
299
|
+
error: error.message
|
|
300
|
+
});
|
|
301
|
+
this.emit('refactoring:error', { type: 'export-removal', export: unusedExport, error });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Remove unused dependencies
|
|
306
|
+
const depsToRemove = changes.removeUnusedDependencies !== false
|
|
307
|
+
? this.analysisResults.unusedDependencies
|
|
308
|
+
: [];
|
|
309
|
+
|
|
310
|
+
for (const dep of depsToRemove) {
|
|
311
|
+
try {
|
|
312
|
+
const absPath = path.resolve(this.engine.context.cwd, dep.manifest);
|
|
313
|
+
// TODO: Implement package.json dependency removal
|
|
314
|
+
refactoringResults.dependenciesRemoved.push(dep);
|
|
315
|
+
this.emit('refactoring:dependency-removed', dep);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
refactoringResults.errors.push({
|
|
318
|
+
type: 'dependency-removal',
|
|
319
|
+
package: dep.package,
|
|
320
|
+
error: error.message
|
|
321
|
+
});
|
|
322
|
+
this.emit('refactoring:error', { type: 'dependency-removal', package: dep.package, error });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
this.isRunning = false;
|
|
328
|
+
this.emit('refactoring:complete', refactoringResults);
|
|
329
|
+
return refactoringResults;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
this.isRunning = false;
|
|
332
|
+
this.emit('refactoring:error', { error });
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get current analysis metrics
|
|
339
|
+
* @returns {Object} Metrics
|
|
340
|
+
*/
|
|
341
|
+
getMetrics() {
|
|
342
|
+
if (!this.engine) {
|
|
343
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
344
|
+
}
|
|
345
|
+
return this.engine.context.metrics;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get all registered plugins
|
|
350
|
+
* @returns {Array} Plugin instances
|
|
351
|
+
*/
|
|
352
|
+
getPlugins() {
|
|
353
|
+
if (!this.engine) {
|
|
354
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
355
|
+
}
|
|
356
|
+
return this.engine.context.pluginRegistry?.getPlugins() || [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get analysis results
|
|
361
|
+
* @returns {Object} Analysis results
|
|
362
|
+
*/
|
|
363
|
+
getAnalysisResults() {
|
|
364
|
+
return this.analysisResults;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if API is currently running
|
|
369
|
+
* @returns {boolean}
|
|
370
|
+
*/
|
|
371
|
+
isProcessing() {
|
|
372
|
+
return this.isRunning;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export default HeadlessAPI;
|