arcvision 0.2.12 → 0.2.15
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/ARCVISION_DIRECTORY_STRUCTURE.md +104 -0
- package/CLI_STRUCTURE.md +110 -0
- package/CONFIGURATION.md +119 -0
- package/IMPLEMENTATION_SUMMARY.md +99 -0
- package/README.md +149 -89
- package/architecture.authority.ledger.json +46 -0
- package/arcvision-0.2.3.tgz +0 -0
- package/arcvision-0.2.4.tgz +0 -0
- package/arcvision-0.2.5.tgz +0 -0
- package/arcvision.context.diff.json +2181 -0
- package/arcvision.context.json +1021 -0
- package/arcvision.context.v1.json +2163 -0
- package/arcvision.context.v2.json +2173 -0
- package/arcvision_context/README.md +93 -0
- package/arcvision_context/architecture.authority.ledger.json +83 -0
- package/arcvision_context/arcvision.context.json +6884 -0
- package/debug-cycle-detection.js +56 -0
- package/dist/index.js +1626 -25
- package/docs/ENHANCED_ACCURACY_SAFETY_PROTOCOL.md +172 -0
- package/docs/accuracy-enhancement-artifacts/enhanced-validation-config.json +98 -0
- package/docs/acig-robustness-guide.md +164 -0
- package/docs/authoritative-gate-implementation.md +168 -0
- package/docs/cli-strengthening-summary.md +232 -0
- package/docs/invariant-system-summary.md +100 -0
- package/docs/invariant-system.md +112 -0
- package/generate_large_test.js +42 -0
- package/large_test_repo.json +1 -0
- package/output1.json +2163 -0
- package/output2.json +2163 -0
- package/package.json +46 -36
- package/scan_calcom_report.txt +0 -0
- package/scan_leafmint_report.txt +0 -0
- package/scan_output.txt +0 -0
- package/scan_trigger_report.txt +0 -0
- package/schema/arcvision_context_schema_v1.json +136 -1
- package/src/arcvision-guard.js +433 -0
- package/src/core/authority-core-detector.js +382 -0
- package/src/core/authority-ledger.js +300 -0
- package/src/core/blastRadius.js +299 -0
- package/src/core/call-resolver.js +196 -0
- package/src/core/change-evaluator.js +509 -0
- package/src/core/change-evaluator.js.backup +424 -0
- package/src/core/change-evaluator.ts +285 -0
- package/src/core/chunked-uploader.js +180 -0
- package/src/core/circular-dependency-detector.js +404 -0
- package/src/core/cli-error-handler.js +458 -0
- package/src/core/cli-validator.js +458 -0
- package/src/core/compression.js +64 -0
- package/src/core/context_builder.js +741 -0
- package/src/core/dependency-manager.js +134 -0
- package/src/core/di-detector.js +202 -0
- package/src/core/diff-analyzer.js +76 -0
- package/src/core/example-invariants.js +135 -0
- package/src/core/failure-mode-synthesizer.js +341 -0
- package/src/core/invariant-analyzer.js +294 -0
- package/src/core/invariant-detector.js +548 -0
- package/src/core/invariant-enforcer.js +171 -0
- package/src/core/invariant-evaluation-utils.js +172 -0
- package/src/core/invariant-hooks.js +152 -0
- package/src/core/invariant-integration-example.js +186 -0
- package/src/core/invariant-registry.js +298 -0
- package/src/core/invariant-registry.ts +100 -0
- package/src/core/invariant-types.js +66 -0
- package/src/core/invariants-index.js +88 -0
- package/src/core/method-tracker.js +170 -0
- package/src/core/override-handler.js +304 -0
- package/src/core/ownership-resolver.js +227 -0
- package/src/core/parser-enhanced.js +80 -0
- package/src/core/parser.js +610 -0
- package/src/core/path-resolver.js +240 -0
- package/src/core/pattern-matcher.js +246 -0
- package/src/core/progress-tracker.js +71 -0
- package/src/core/react-nextjs-detector.js +245 -0
- package/src/core/readme-generator.js +167 -0
- package/src/core/retry-handler.js +57 -0
- package/src/core/scanner.js +289 -0
- package/src/core/semantic-analyzer.js +204 -0
- package/src/core/structural-context-owner.js +442 -0
- package/src/core/symbol-indexer.js +164 -0
- package/src/core/tsconfig-utils.js +73 -0
- package/src/core/type-analyzer.js +272 -0
- package/src/core/watcher.js +18 -0
- package/src/core/workspace-scanner.js +88 -0
- package/src/engine/context_builder.js +280 -0
- package/src/engine/context_sorter.js +59 -0
- package/src/engine/context_validator.js +200 -0
- package/src/engine/id-generator.js +16 -0
- package/src/engine/pass1_facts.js +260 -0
- package/src/engine/pass2_semantics.js +333 -0
- package/src/engine/pass3_lifter.js +99 -0
- package/src/engine/pass4_signals.js +201 -0
- package/src/index.js +830 -0
- package/src/plugins/express-plugin.js +48 -0
- package/src/plugins/plugin-manager.js +58 -0
- package/src/plugins/react-plugin.js +54 -0
- package/temp_original.js +0 -0
- package/test/determinism-test.js +83 -0
- package/test-authoritative-context.js +53 -0
- package/test-real-authoritative-context.js +118 -0
- package/test-upload-enhancements.js +111 -0
- package/test_repos/allowed-clean-architecture/.arcvision/invariants.json +57 -0
- package/test_repos/allowed-clean-architecture/adapters/controllers/UserController.js +95 -0
- package/test_repos/allowed-clean-architecture/adapters/http/HttpServer.js +78 -0
- package/test_repos/allowed-clean-architecture/application/dtos/CreateUserRequest.js +37 -0
- package/test_repos/allowed-clean-architecture/application/services/UserService.js +61 -0
- package/test_repos/allowed-clean-architecture/arcvision_context/README.md +93 -0
- package/test_repos/allowed-clean-architecture/arcvision_context/arcvision.context.json +2796 -0
- package/test_repos/allowed-clean-architecture/domain/interfaces/UserRepository.js +25 -0
- package/test_repos/allowed-clean-architecture/domain/models/User.js +39 -0
- package/test_repos/allowed-clean-architecture/index.js +45 -0
- package/test_repos/allowed-clean-architecture/infrastructure/database/DatabaseConnection.js +56 -0
- package/test_repos/allowed-clean-architecture/infrastructure/repositories/InMemoryUserRepository.js +61 -0
- package/test_repos/allowed-clean-architecture/package.json +15 -0
- package/test_repos/blocked-legacy-monolith/.arcvision/invariants.json +78 -0
- package/test_repos/blocked-legacy-monolith/arcvision_context/README.md +93 -0
- package/test_repos/blocked-legacy-monolith/arcvision_context/arcvision.context.json +2882 -0
- package/test_repos/blocked-legacy-monolith/database/dbConnection.js +35 -0
- package/test_repos/blocked-legacy-monolith/index.js +38 -0
- package/test_repos/blocked-legacy-monolith/modules/emailService.js +31 -0
- package/test_repos/blocked-legacy-monolith/modules/paymentProcessor.js +37 -0
- package/test_repos/blocked-legacy-monolith/package.json +15 -0
- package/test_repos/blocked-legacy-monolith/shared/utils.js +19 -0
- package/test_repos/blocked-legacy-monolith/utils/helpers.js +23 -0
- package/test_repos/risky-microservices-concerns/.arcvision/invariants.json +69 -0
- package/test_repos/risky-microservices-concerns/arcvision_context/README.md +93 -0
- package/test_repos/risky-microservices-concerns/arcvision_context/arcvision.context.json +3070 -0
- package/test_repos/risky-microservices-concerns/common/utils.js +77 -0
- package/test_repos/risky-microservices-concerns/gateways/apiGateway.js +84 -0
- package/test_repos/risky-microservices-concerns/index.js +20 -0
- package/test_repos/risky-microservices-concerns/libs/deprecatedHelper.js +36 -0
- package/test_repos/risky-microservices-concerns/package.json +15 -0
- package/test_repos/risky-microservices-concerns/services/orderService.js +42 -0
- package/test_repos/risky-microservices-concerns/services/userService.js +48 -0
- package/verify_engine.js +116 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast Radius Analysis
|
|
3
|
+
*
|
|
4
|
+
* This module computes the blast radius for each file in the architecture map.
|
|
5
|
+
* Blast radius is defined as the number of files that import a given file.
|
|
6
|
+
* It also includes transitive dependencies analysis for more comprehensive impact assessment.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds a reverse dependency graph from the architecture map
|
|
11
|
+
* @param {Object} architectureMap - The architecture map with nodes and edges
|
|
12
|
+
* @returns {Object} Reverse dependency map where key is imported file and value is array of importing files
|
|
13
|
+
*/
|
|
14
|
+
function buildReverseDependencyGraph(architectureMap) {
|
|
15
|
+
const reverseGraph = {};
|
|
16
|
+
|
|
17
|
+
// Initialize all files in the reverse graph
|
|
18
|
+
if (architectureMap.nodes && Array.isArray(architectureMap.nodes)) {
|
|
19
|
+
architectureMap.nodes.forEach(node => {
|
|
20
|
+
const filePath = node.id;
|
|
21
|
+
if (filePath && !reverseGraph[filePath]) {
|
|
22
|
+
reverseGraph[filePath] = [];
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build reverse dependencies from edges
|
|
28
|
+
if (architectureMap.edges && Array.isArray(architectureMap.edges)) {
|
|
29
|
+
architectureMap.edges.forEach(edge => {
|
|
30
|
+
if (edge.source && edge.target) {
|
|
31
|
+
const importingFile = edge.source; // file that imports
|
|
32
|
+
const importedFile = edge.target; // file that is imported
|
|
33
|
+
|
|
34
|
+
if (!reverseGraph[importedFile]) {
|
|
35
|
+
reverseGraph[importedFile] = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add the importing file to the list of files that depend on importedFile
|
|
39
|
+
if (!reverseGraph[importedFile].includes(importingFile)) {
|
|
40
|
+
reverseGraph[importedFile].push(importingFile);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return reverseGraph;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Computes blast radius for each file
|
|
51
|
+
* @param {Object} reverseGraph - The reverse dependency graph
|
|
52
|
+
* @returns {Object} Map of file paths to their blast radius (number of files that import them)
|
|
53
|
+
*/
|
|
54
|
+
function computeBlastRadius(reverseGraph) {
|
|
55
|
+
const blastRadiusMap = {};
|
|
56
|
+
|
|
57
|
+
for (const [filePath, importingFiles] of Object.entries(reverseGraph)) {
|
|
58
|
+
blastRadiusMap[filePath] = Array.isArray(importingFiles) ? importingFiles.length : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return blastRadiusMap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Computes transitive blast radius (indirect dependencies)
|
|
66
|
+
* @param {Object} reverseGraph - The reverse dependency graph
|
|
67
|
+
* @param {Object} directBlastRadius - Direct blast radius map
|
|
68
|
+
* @returns {Object} Map of file paths to their total blast radius (direct + transitive)
|
|
69
|
+
*/
|
|
70
|
+
function computeTransitiveBlastRadius(reverseGraph, directBlastRadius) {
|
|
71
|
+
const transitiveBlastRadius = {};
|
|
72
|
+
|
|
73
|
+
for (const [filePath] of Object.entries(reverseGraph)) {
|
|
74
|
+
// Calculate transitive dependencies using a breadth-first search
|
|
75
|
+
const visited = new Set();
|
|
76
|
+
const queue = [...(reverseGraph[filePath] || [])];
|
|
77
|
+
let transitiveCount = 0;
|
|
78
|
+
|
|
79
|
+
while (queue.length > 0) {
|
|
80
|
+
const currentFile = queue.shift();
|
|
81
|
+
|
|
82
|
+
if (!visited.has(currentFile)) {
|
|
83
|
+
visited.add(currentFile);
|
|
84
|
+
transitiveCount++;
|
|
85
|
+
|
|
86
|
+
// Add dependencies of this file to the queue
|
|
87
|
+
if (reverseGraph[currentFile] && Array.isArray(reverseGraph[currentFile])) {
|
|
88
|
+
queue.push(...reverseGraph[currentFile]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Total blast radius = direct + transitive
|
|
94
|
+
transitiveBlastRadius[filePath] = (directBlastRadius[filePath] || 0) + transitiveCount;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return transitiveBlastRadius;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Finds the file with the highest blast radius
|
|
102
|
+
* @param {Object} blastRadiusMap - Map of file paths to their blast radius
|
|
103
|
+
* @returns {Object|null} Object containing the file path and blast radius, or null if no files
|
|
104
|
+
*/
|
|
105
|
+
function findHighestBlastRadius(blastRadiusMap) {
|
|
106
|
+
if (Object.keys(blastRadiusMap).length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let maxFile = null;
|
|
111
|
+
let maxRadius = -1;
|
|
112
|
+
|
|
113
|
+
for (const [filePath, radius] of Object.entries(blastRadiusMap)) {
|
|
114
|
+
if (radius > maxRadius) {
|
|
115
|
+
maxRadius = radius;
|
|
116
|
+
maxFile = { file: filePath, blast_radius: radius };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return maxFile;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Gets the top N files by blast radius
|
|
125
|
+
* @param {Object} blastRadiusMap - Map of file paths to their blast radius
|
|
126
|
+
* @param {number} limit - Number of top files to return (default: 3)
|
|
127
|
+
* @returns {Array} Array of top N files with their blast radius
|
|
128
|
+
*/
|
|
129
|
+
function getTopBlastRadiusFiles(blastRadiusMap, limit = 3) {
|
|
130
|
+
if (Object.keys(blastRadiusMap).length === 0) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Convert to array and sort by blast radius descending
|
|
135
|
+
const sortedFiles = Object.entries(blastRadiusMap)
|
|
136
|
+
.map(([file, blast_radius]) => ({ file, blast_radius }))
|
|
137
|
+
.sort((a, b) => {
|
|
138
|
+
// Sort by blast radius first, then by filename for consistency
|
|
139
|
+
if (b.blast_radius !== a.blast_radius) {
|
|
140
|
+
return b.blast_radius - a.blast_radius;
|
|
141
|
+
}
|
|
142
|
+
return a.file.localeCompare(b.file);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return sortedFiles.slice(0, limit);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Computes blast radius results with percentage of total files
|
|
150
|
+
* @param {Object} blastRadiusMap - Map of file paths to their blast radius
|
|
151
|
+
* @param {number} totalFiles - Total number of files in the project
|
|
152
|
+
* @returns {Array} Array of files with blast radius and percentage
|
|
153
|
+
*/
|
|
154
|
+
function computeBlastRadiusWithPercentage(blastRadiusMap, totalFiles) {
|
|
155
|
+
if (Object.keys(blastRadiusMap).length === 0 || totalFiles === 0) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.entries(blastRadiusMap)
|
|
160
|
+
.map(([file, blastRadius]) => {
|
|
161
|
+
const percentOfGraph = Number(((blastRadius / totalFiles) * 100).toFixed(2));
|
|
162
|
+
return {
|
|
163
|
+
file,
|
|
164
|
+
blastRadius,
|
|
165
|
+
percentOfGraph
|
|
166
|
+
};
|
|
167
|
+
})
|
|
168
|
+
.sort((a, b) => {
|
|
169
|
+
// Sort by blast radius first, then by filename for consistency
|
|
170
|
+
if (b.blastRadius !== a.blastRadius) {
|
|
171
|
+
return b.blastRadius - a.blastRadius;
|
|
172
|
+
}
|
|
173
|
+
return a.file.localeCompare(b.file);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Analyzes the criticality of files based on multiple factors
|
|
179
|
+
* @param {Object} blastRadiusMap - Direct blast radius map
|
|
180
|
+
* @param {Object} reverseGraph - The reverse dependency graph
|
|
181
|
+
* @param {Array} nodes - The list of nodes in the architecture map
|
|
182
|
+
* @returns {Array} Array of files with criticality scores
|
|
183
|
+
*/
|
|
184
|
+
function analyzeCriticality(blastRadiusMap, reverseGraph, nodes) {
|
|
185
|
+
const criticalityAnalysis = [];
|
|
186
|
+
|
|
187
|
+
for (const [file, blastRadius] of Object.entries(blastRadiusMap)) {
|
|
188
|
+
const node = nodes.find(n => n.id === file);
|
|
189
|
+
|
|
190
|
+
// Calculate various metrics for criticality
|
|
191
|
+
const dependencies = reverseGraph[file] ? reverseGraph[file].length : 0; // How many files depend on this file
|
|
192
|
+
const dependencyStrength = calculateDependencyStrength(reverseGraph, file);
|
|
193
|
+
|
|
194
|
+
// Determine criticality score based on multiple factors
|
|
195
|
+
let criticalityScore = blastRadius; // Start with direct blast radius
|
|
196
|
+
|
|
197
|
+
// Add weight for dependency strength
|
|
198
|
+
criticalityScore += dependencyStrength * 0.5;
|
|
199
|
+
|
|
200
|
+
// Add weight if the file has special characteristics (has functions, exports, etc.)
|
|
201
|
+
if (node && node.metadata) {
|
|
202
|
+
if (node.metadata.functions && node.metadata.functions.length > 0) {
|
|
203
|
+
criticalityScore += 1; // Files with functions are more critical
|
|
204
|
+
}
|
|
205
|
+
if (node.metadata.classes && node.metadata.classes.length > 0) {
|
|
206
|
+
criticalityScore += 1.5; // Classes tend to be more central
|
|
207
|
+
}
|
|
208
|
+
if (node.metadata.types && node.metadata.types.length > 0) {
|
|
209
|
+
criticalityScore += 0.5; // Types are foundational
|
|
210
|
+
}
|
|
211
|
+
if (node.metadata.exports && node.metadata.exports.length > 0) {
|
|
212
|
+
criticalityScore += 0.7; // Exporting files are more critical
|
|
213
|
+
}
|
|
214
|
+
if (node.metadata.apiCalls && node.metadata.apiCalls.length > 0) {
|
|
215
|
+
criticalityScore += 1.2; // API calls connect to external systems
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add weight based on file path patterns (e.g., auth, core, utils)
|
|
220
|
+
if (file.toLowerCase().includes('auth')) {
|
|
221
|
+
criticalityScore += 2;
|
|
222
|
+
} else if (file.toLowerCase().includes('core') || file.toLowerCase().includes('util') || file.toLowerCase().includes('utils')) {
|
|
223
|
+
criticalityScore += 1.5;
|
|
224
|
+
} else if (file.toLowerCase().includes('config') || file.toLowerCase().includes('index')) {
|
|
225
|
+
criticalityScore += 0.8;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Boost score for files with higher blast radius
|
|
229
|
+
if (blastRadius > 10) {
|
|
230
|
+
criticalityScore *= 1.8; // Very high impact files get more boost
|
|
231
|
+
} else if (blastRadius > 5) {
|
|
232
|
+
criticalityScore *= 1.5; // Higher impact files get a boost
|
|
233
|
+
} else if (blastRadius > 0) {
|
|
234
|
+
criticalityScore *= 1.1; // Any connected file gets slight boost
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Additional factors for criticality
|
|
238
|
+
// Files with many outgoing dependencies are also critical
|
|
239
|
+
if (node && node.dependencies && node.dependencies.length > 5) {
|
|
240
|
+
criticalityScore += 2; // Highly coupled files
|
|
241
|
+
} else if (node && node.dependencies && node.dependencies.length > 10) {
|
|
242
|
+
criticalityScore += 5; // Extremely coupled files
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Files that appear in multiple contexts (e.g. shared folders)
|
|
246
|
+
if (file.includes('/shared/') || file.includes('/common/') || file.includes('/utils/') || file.includes('/lib/')) {
|
|
247
|
+
criticalityScore += 1.5; // Shared components are typically more critical
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
criticalityAnalysis.push({
|
|
251
|
+
file,
|
|
252
|
+
blastRadius,
|
|
253
|
+
dependencies,
|
|
254
|
+
dependencyStrength,
|
|
255
|
+
criticalityScore: Number(criticalityScore.toFixed(2)),
|
|
256
|
+
isHub: blastRadius > 5 // Consider files with blast radius > 5 as hubs
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Sort by criticality score descending, then by file name ascending
|
|
261
|
+
return criticalityAnalysis.sort((a, b) => {
|
|
262
|
+
if (b.criticalityScore !== a.criticalityScore) {
|
|
263
|
+
return b.criticalityScore - a.criticalityScore;
|
|
264
|
+
}
|
|
265
|
+
return a.file.localeCompare(b.file);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Calculates the strength of dependencies for a file (how many dependencies it has)
|
|
271
|
+
* @param {Object} reverseGraph - The reverse dependency graph
|
|
272
|
+
* @param {string} file - The file to analyze
|
|
273
|
+
* @returns {number} The dependency strength score
|
|
274
|
+
*/
|
|
275
|
+
function calculateDependencyStrength(reverseGraph, file) {
|
|
276
|
+
if (!reverseGraph[file] || !Array.isArray(reverseGraph[file])) {
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Calculate the cumulative strength of dependencies
|
|
281
|
+
let strength = 0;
|
|
282
|
+
for (const dep of reverseGraph[file]) {
|
|
283
|
+
if (reverseGraph[dep]) {
|
|
284
|
+
strength += reverseGraph[dep].length; // How many files the dependency affects
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return strength;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
buildReverseDependencyGraph,
|
|
293
|
+
computeBlastRadius,
|
|
294
|
+
computeTransitiveBlastRadius,
|
|
295
|
+
findHighestBlastRadius,
|
|
296
|
+
getTopBlastRadiusFiles,
|
|
297
|
+
computeBlastRadiusWithPercentage,
|
|
298
|
+
analyzeCriticality
|
|
299
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves CallExpressions and NewExpressions to symbols using the import graph.
|
|
3
|
+
* Emits typed edges: CALLS, INSTANTIATES
|
|
4
|
+
*/
|
|
5
|
+
class CallResolver {
|
|
6
|
+
constructor(symbolIndex, fileMap) {
|
|
7
|
+
this.symbolIndex = symbolIndex; // Map<symbolId, Symbol>
|
|
8
|
+
this.fileMap = fileMap; // Map<fileId, FileObject> (to access imports)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve calls in all files
|
|
13
|
+
* @returns {Array<Object>} List of edges
|
|
14
|
+
*/
|
|
15
|
+
resolve(rawFiles) {
|
|
16
|
+
const edges = [];
|
|
17
|
+
console.log(`Resolving calls for ${rawFiles.length} files...`);
|
|
18
|
+
|
|
19
|
+
for (const file of rawFiles) {
|
|
20
|
+
const fileEdges = this._resolveFile(file);
|
|
21
|
+
edges.push(...fileEdges);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(`Resolved ${edges.length} call/instantiate edges.`);
|
|
25
|
+
return edges;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_resolveFile(file) {
|
|
29
|
+
const edges = [];
|
|
30
|
+
if (!file.methodCalls) return edges; // generated by method-tracker.js
|
|
31
|
+
|
|
32
|
+
const fileId = file.id;
|
|
33
|
+
const { functionCalls, constructorCalls, methodCalls } = file.methodCalls;
|
|
34
|
+
const imports = file.metadata.imports || [];
|
|
35
|
+
|
|
36
|
+
// Helper to find imported symbol source
|
|
37
|
+
const resolveImportedSymbol = (symbolName) => {
|
|
38
|
+
// Check imports
|
|
39
|
+
for (const imp of imports) {
|
|
40
|
+
// imp.specifiers: [{local, imported}]
|
|
41
|
+
if (imp.specifiers) {
|
|
42
|
+
const spec = imp.specifiers.find(s => s.local === symbolName);
|
|
43
|
+
if (spec) {
|
|
44
|
+
// Found import. Now we need the resolved file path of this import.
|
|
45
|
+
// This requires the 'resolvedPath' to be present on the import object.
|
|
46
|
+
// This implies PathResolver must run BEFORE CallResolver and annotate imports.
|
|
47
|
+
if (imp.resolvedPath) {
|
|
48
|
+
const targetFileId = imp.resolvedPath;
|
|
49
|
+
const targetSymbolName = spec.imported === 'default' ? 'default' : spec.imported; // Or default export name?
|
|
50
|
+
|
|
51
|
+
// Construct potential symbol ID
|
|
52
|
+
// Note: If target is a file, we look for 'targetFileId::targetSymbolName'
|
|
53
|
+
// Special handling for default exports might be needed if they are named differently in target.
|
|
54
|
+
// For now assuming named exports match or 'default'.
|
|
55
|
+
|
|
56
|
+
// Retry finding the exact symbol ID in the target file
|
|
57
|
+
// This usually requires a lookup in fileToSymbols or just constructing ID if deterministic
|
|
58
|
+
|
|
59
|
+
// Attempt 1: Named export
|
|
60
|
+
let candidateId = `${targetFileId}::${targetSymbolName}`;
|
|
61
|
+
if (this.symbolIndex.has(candidateId)) return candidateId;
|
|
62
|
+
|
|
63
|
+
// Attempt 2: If default import, we need to find what is exported as default
|
|
64
|
+
if (spec.imported === 'default') {
|
|
65
|
+
// Find the symbol in target file that has type 'default' or name 'default'
|
|
66
|
+
// The SymbolIndexer indexes 'default' if it's named 'default' or if we handled it.
|
|
67
|
+
candidateId = `${targetFileId}::default`;
|
|
68
|
+
if (this.symbolIndex.has(candidateId)) return candidateId;
|
|
69
|
+
|
|
70
|
+
// fallback: look for ANY default export in target file via SymbolIndex iteration (expensive but necessary?)
|
|
71
|
+
// Better: SymbolIndexer ensure 'default' is the name for default exports.
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Additional attempts for common patterns
|
|
75
|
+
// Attempt 3: Try with common function prefixes
|
|
76
|
+
const commonPatterns = [
|
|
77
|
+
`${targetFileId}::use${targetSymbolName.charAt(0).toUpperCase() + targetSymbolName.slice(1)}`,
|
|
78
|
+
`${targetFileId}::get${targetSymbolName.charAt(0).toUpperCase() + targetSymbolName.slice(1)}`,
|
|
79
|
+
`${targetFileId}::create${targetSymbolName.charAt(0).toUpperCase() + targetSymbolName.slice(1)}`,
|
|
80
|
+
`${targetFileId}::_${targetSymbolName}`, // underscore prefix
|
|
81
|
+
`${targetFileId}::${targetSymbolName}Impl`, // implementation suffix
|
|
82
|
+
`${targetFileId}::${targetSymbolName}Handler` // handler suffix
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const patternId of commonPatterns) {
|
|
86
|
+
if (this.symbolIndex.has(patternId)) return patternId;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If not imported, it might be local
|
|
94
|
+
const localId = `${fileId}::${symbolName}`;
|
|
95
|
+
if (this.symbolIndex.has(localId)) {
|
|
96
|
+
return localId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Additional local patterns
|
|
100
|
+
const localPatterns = [
|
|
101
|
+
`${fileId}::use${symbolName.charAt(0).toUpperCase() + symbolName.slice(1)}`,
|
|
102
|
+
`${fileId}::get${symbolName.charAt(0).toUpperCase() + symbolName.slice(1)}`,
|
|
103
|
+
`${fileId}::create${symbolName.charAt(0).toUpperCase() + symbolName.slice(1)}`,
|
|
104
|
+
`${fileId}::_${symbolName}`, // underscore prefix
|
|
105
|
+
`${fileId}::${symbolName}Impl`, // implementation suffix
|
|
106
|
+
`${fileId}::${symbolName}Handler` // handler suffix
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const patternId of localPatterns) {
|
|
110
|
+
if (this.symbolIndex.has(patternId)) return patternId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// 1. Resolve Function Calls
|
|
117
|
+
if (functionCalls) {
|
|
118
|
+
for (const call of functionCalls) {
|
|
119
|
+
const targetSymbolId = resolveImportedSymbol(call.name);
|
|
120
|
+
if (targetSymbolId) {
|
|
121
|
+
edges.push({
|
|
122
|
+
sourceFileId: fileId,
|
|
123
|
+
targetSymbolId: targetSymbolId,
|
|
124
|
+
type: 'CALLS',
|
|
125
|
+
line: call.loc ? call.loc.start.line : 0,
|
|
126
|
+
confidence: 1.0
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 2. Resolve Constructor Calls
|
|
133
|
+
if (constructorCalls) {
|
|
134
|
+
for (const call of constructorCalls) {
|
|
135
|
+
const targetSymbolId = resolveImportedSymbol(call.className);
|
|
136
|
+
if (targetSymbolId) {
|
|
137
|
+
edges.push({
|
|
138
|
+
sourceFileId: fileId,
|
|
139
|
+
targetSymbolId: targetSymbolId,
|
|
140
|
+
type: 'INSTANTIATES',
|
|
141
|
+
line: call.loc ? call.loc.start.line : 0,
|
|
142
|
+
confidence: 1.0
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 3. Resolve Method Calls (Namespace & Object Property Resolution)
|
|
149
|
+
// e.g., import * as api from './api'; api.fetchData();
|
|
150
|
+
// OR import { actions } from './actions'; actions.login();
|
|
151
|
+
if (methodCalls) {
|
|
152
|
+
for (const call of methodCalls) {
|
|
153
|
+
const imp = imports.find(i => i.specifiers?.some(s => s.local === call.object));
|
|
154
|
+
if (imp && imp.resolvedPath) {
|
|
155
|
+
const targetFileId = imp.resolvedPath;
|
|
156
|
+
const spec = imp.specifiers.find(s => s.local === call.object);
|
|
157
|
+
const importedAs = spec ? spec.imported : null;
|
|
158
|
+
|
|
159
|
+
// Strategy A: Namespace Resolution
|
|
160
|
+
let candidateId = `${targetFileId}::${call.method}`;
|
|
161
|
+
if (this.symbolIndex.has(candidateId)) {
|
|
162
|
+
edges.push({
|
|
163
|
+
sourceFileId: fileId, targetSymbolId: candidateId,
|
|
164
|
+
type: 'CALLS', line: call.loc ? call.loc.start.line : 0, confidence: 1.0
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Strategy B: Object Property Resolution
|
|
170
|
+
if (importedAs) {
|
|
171
|
+
const targetSymbolName = importedAs === 'default' ? 'default' : importedAs;
|
|
172
|
+
candidateId = `${targetFileId}::${targetSymbolName}.${call.method}`;
|
|
173
|
+
if (this.symbolIndex.has(candidateId)) {
|
|
174
|
+
edges.push({
|
|
175
|
+
sourceFileId: fileId, targetSymbolId: candidateId,
|
|
176
|
+
type: 'CALLS', line: call.loc ? call.loc.start.line : 0, confidence: 1.0
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const localObjectId = `${fileId}::${call.object}.${call.method}`;
|
|
183
|
+
if (this.symbolIndex.has(localObjectId)) {
|
|
184
|
+
edges.push({
|
|
185
|
+
sourceFileId: fileId, targetSymbolId: localObjectId,
|
|
186
|
+
type: 'CALLS', line: call.loc ? call.loc.start.line : 0, confidence: 1.0
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return edges;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { CallResolver };
|