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,180 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { Buffer } = require('buffer');
|
|
3
|
+
|
|
4
|
+
class ChunkedUploader {
|
|
5
|
+
constructor(chunkSize = 1024 * 1024 * 4) { // 4MB default chunk size
|
|
6
|
+
this.chunkSize = chunkSize;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Calculate approximate byte size of a JSON object
|
|
11
|
+
*/
|
|
12
|
+
calculateByteSize(obj) {
|
|
13
|
+
return Buffer.byteLength(JSON.stringify(obj));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Split a large JSON object into chunks
|
|
18
|
+
*/
|
|
19
|
+
splitIntoChunks(data, maxChunkSize = this.chunkSize) {
|
|
20
|
+
const jsonString = JSON.stringify(data);
|
|
21
|
+
const chunks = [];
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < jsonString.length; i += maxChunkSize) {
|
|
24
|
+
const chunk = jsonString.slice(i, i + maxChunkSize);
|
|
25
|
+
chunks.push({
|
|
26
|
+
data: chunk,
|
|
27
|
+
index: Math.floor(i / maxChunkSize),
|
|
28
|
+
totalChunks: Math.ceil(jsonString.length / maxChunkSize)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return chunks;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reconstruct data from chunks
|
|
37
|
+
*/
|
|
38
|
+
reconstructFromChunks(chunks) {
|
|
39
|
+
// Sort chunks by index to ensure correct order
|
|
40
|
+
chunks.sort((a, b) => a.index - b.index);
|
|
41
|
+
const reconstructedString = chunks.map(chunk => chunk.data).join('');
|
|
42
|
+
return JSON.parse(reconstructedString);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Upload data in chunks to the server
|
|
47
|
+
*/
|
|
48
|
+
async uploadInChunks(data, token, apiUrl) {
|
|
49
|
+
const totalSize = this.calculateByteSize(data);
|
|
50
|
+
const chunks = this.splitIntoChunks(data);
|
|
51
|
+
|
|
52
|
+
console.log(chalk.blue(`Preparing to upload ${chunks.length} chunks (${Math.round(totalSize / 1024 / 1024)} MB total)`));
|
|
53
|
+
|
|
54
|
+
// First, initiate the chunked upload session
|
|
55
|
+
const sessionId = await this.initiateSession(chunks.length, token, apiUrl);
|
|
56
|
+
|
|
57
|
+
if (!sessionId) {
|
|
58
|
+
throw new Error('Failed to initiate upload session');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Upload each chunk individually
|
|
62
|
+
const uploadPromises = chunks.map(async (chunk, index) => {
|
|
63
|
+
const progress = ((index + 1) / chunks.length) * 100;
|
|
64
|
+
console.log(chalk.yellow(`Uploading chunk ${index + 1}/${chunks.length} (${Math.round(progress)}%)`));
|
|
65
|
+
|
|
66
|
+
return this.uploadChunk(chunk, sessionId, token, apiUrl);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Wait for all chunks to upload
|
|
70
|
+
const results = await Promise.all(uploadPromises);
|
|
71
|
+
|
|
72
|
+
// Verify all chunks were uploaded successfully
|
|
73
|
+
const allSuccessful = results.every(result => result.success);
|
|
74
|
+
|
|
75
|
+
if (!allSuccessful) {
|
|
76
|
+
throw new Error('Some chunks failed to upload');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Finalize the upload
|
|
80
|
+
const finalResult = await this.finalizeUpload(sessionId, token, apiUrl);
|
|
81
|
+
|
|
82
|
+
return finalResult;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initiate a chunked upload session
|
|
87
|
+
*/
|
|
88
|
+
async initiateSession(totalChunks, token, apiUrl) {
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(`${apiUrl}/api/upload/initiate`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
'Authorization': `Bearer ${token}`
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
totalChunks,
|
|
98
|
+
timestamp: Date.now()
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`Session initiation failed: ${response.status}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await response.json();
|
|
107
|
+
return result.sessionId;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(chalk.red(`Failed to initiate upload session: ${error.message}`));
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Upload a single chunk
|
|
116
|
+
*/
|
|
117
|
+
async uploadChunk(chunk, sessionId, token, apiUrl) {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`${apiUrl}/api/upload/chunk`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
'Authorization': `Bearer ${token}`
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
sessionId,
|
|
127
|
+
chunkIndex: chunk.index,
|
|
128
|
+
chunkData: chunk.data,
|
|
129
|
+
totalChunks: chunk.totalChunks
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await response.json();
|
|
134
|
+
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
console.error(chalk.red(`Chunk ${chunk.index} upload failed: ${response.status}`));
|
|
137
|
+
return { success: false, error: result.error || response.statusText };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { success: true, chunkIndex: chunk.index };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(chalk.red(`Chunk ${chunk.index} upload error: ${error.message}`));
|
|
143
|
+
return { success: false, error: error.message };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Finalize the chunked upload
|
|
149
|
+
*/
|
|
150
|
+
async finalizeUpload(sessionId, token, apiUrl) {
|
|
151
|
+
try {
|
|
152
|
+
console.log(chalk.yellow('Finalizing upload...'));
|
|
153
|
+
|
|
154
|
+
const response = await fetch(`${apiUrl}/api/upload/finalize`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
'Authorization': `Bearer ${token}`
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
sessionId
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error(`Finalization failed: ${response.status}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = await response.json();
|
|
170
|
+
return result;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(chalk.red(`Upload finalization failed: ${error.message}`));
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
ChunkedUploader
|
|
180
|
+
};
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Circular Dependency Detector
|
|
3
|
+
* Detects bidirectional dependencies between modules
|
|
4
|
+
* Enhanced to catch all circular dependency patterns including transitive cycles
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { patternMatcher } = require('./pattern-matcher');
|
|
8
|
+
|
|
9
|
+
class CircularDependencyDetector {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.detectedCycles = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect circular dependencies in the dependency graph
|
|
16
|
+
* Enhanced to detect all types of cycles including transitive dependencies
|
|
17
|
+
* @param {Array} nodes - Array of node objects
|
|
18
|
+
* @param {Array} edges - Array of edge objects
|
|
19
|
+
* @returns {Array} Array of detected circular dependencies
|
|
20
|
+
*/
|
|
21
|
+
detectCircularDependencies(nodes, edges) {
|
|
22
|
+
const cycles = [];
|
|
23
|
+
const visited = new Set();
|
|
24
|
+
const recStack = new Set();
|
|
25
|
+
const adjList = new Map();
|
|
26
|
+
|
|
27
|
+
// Enhanced data structures for better cycle detection
|
|
28
|
+
const nodePaths = new Map();
|
|
29
|
+
const edgeWeights = new Map();
|
|
30
|
+
|
|
31
|
+
// Build adjacency list with enhanced metadata
|
|
32
|
+
for (const edge of edges) {
|
|
33
|
+
// Handle different edge structures (could be {from, to} or {source, target})
|
|
34
|
+
const fromNode = edge.from || edge.source;
|
|
35
|
+
const toNode = edge.to || edge.target;
|
|
36
|
+
|
|
37
|
+
if (!adjList.has(fromNode)) {
|
|
38
|
+
adjList.set(fromNode, []);
|
|
39
|
+
}
|
|
40
|
+
adjList.get(fromNode).push(toNode);
|
|
41
|
+
|
|
42
|
+
// Store edge weights for importance scoring
|
|
43
|
+
const edgeKey = `${fromNode}-${toNode}`;
|
|
44
|
+
edgeWeights.set(edgeKey, edge.weight || 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Map node IDs to file paths
|
|
48
|
+
nodes.forEach(node => {
|
|
49
|
+
// Handle different node structures (could be {id, path} or other formats)
|
|
50
|
+
const nodeId = node.id || node.name || node.filePath;
|
|
51
|
+
const nodePath = node.path || node.filePath || node.name;
|
|
52
|
+
if (nodeId && nodePath) {
|
|
53
|
+
nodePaths.set(nodeId, nodePath);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Enhanced DFS to detect cycles with comprehensive tracking
|
|
58
|
+
const dfs = (node, path) => {
|
|
59
|
+
if (recStack.has(node)) {
|
|
60
|
+
// Found cycle - get the cycle portion
|
|
61
|
+
const cycleStartIndex = path.indexOf(node);
|
|
62
|
+
if (cycleStartIndex !== -1) {
|
|
63
|
+
const cycleNodes = path.slice(cycleStartIndex);
|
|
64
|
+
const cycleEdges = [];
|
|
65
|
+
|
|
66
|
+
// Calculate cycle metrics
|
|
67
|
+
let totalWeight = 0;
|
|
68
|
+
let maxLength = 0;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < cycleNodes.length; i++) {
|
|
71
|
+
const from = cycleNodes[i];
|
|
72
|
+
const to = cycleNodes[(i + 1) % cycleNodes.length];
|
|
73
|
+
const edgeKey = `${from}-${to}`;
|
|
74
|
+
|
|
75
|
+
cycleEdges.push({ from, to });
|
|
76
|
+
totalWeight += edgeWeights.get(edgeKey) || 1;
|
|
77
|
+
maxLength = Math.max(maxLength, nodePaths.get(from)?.length || 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cycles.push({
|
|
81
|
+
cycle: [...cycleNodes, node],
|
|
82
|
+
edges: cycleEdges,
|
|
83
|
+
length: cycleNodes.length + 1,
|
|
84
|
+
totalWeight,
|
|
85
|
+
maxLength,
|
|
86
|
+
severity: this.calculateCycleSeverity(totalWeight, cycleNodes.length)
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (visited.has(node)) return;
|
|
93
|
+
|
|
94
|
+
visited.add(node);
|
|
95
|
+
recStack.add(node);
|
|
96
|
+
path.push(node);
|
|
97
|
+
|
|
98
|
+
const neighbors = adjList.get(node) || [];
|
|
99
|
+
for (const neighbor of neighbors) {
|
|
100
|
+
dfs(neighbor, [...path]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
recStack.delete(node);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Run enhanced DFS from each unvisited node
|
|
107
|
+
for (const node of adjList.keys()) {
|
|
108
|
+
if (!visited.has(node)) {
|
|
109
|
+
dfs(node, []);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Enhanced cycle reporting with detailed information
|
|
114
|
+
return cycles.map(cycle => ({
|
|
115
|
+
...cycle,
|
|
116
|
+
fileCycle: cycle.cycle.map(nodeId => nodePaths.get(nodeId) || nodeId),
|
|
117
|
+
description: this.formatCycleDescription(cycle, nodePaths),
|
|
118
|
+
impactScore: this.calculateImpactScore(cycle, nodePaths)
|
|
119
|
+
})).sort((a, b) => b.impactScore - a.impactScore); // Sort by impact severity
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a specific forbidden dependency pattern creates a cycle
|
|
124
|
+
* @param {Object} forbiddenDep - Forbidden dependency rule
|
|
125
|
+
* @param {Array} nodes - Array of nodes
|
|
126
|
+
* @param {Array} edges - Array of edges
|
|
127
|
+
* @returns {boolean} True if circular dependency detected
|
|
128
|
+
*/
|
|
129
|
+
checkForbiddenCreatesCycle(forbiddenDep, nodes, edges) {
|
|
130
|
+
// First check if the direct forbidden dependency exists
|
|
131
|
+
const directViolation = edges.some(edge => {
|
|
132
|
+
const sourcePath = this.getNodePath(edge.from, nodes);
|
|
133
|
+
const targetPath = this.getNodePath(edge.to, nodes);
|
|
134
|
+
|
|
135
|
+
const fromMatches = patternMatcher.match(sourcePath, forbiddenDep.from, { matchBase: true });
|
|
136
|
+
const toMatches = patternMatcher.match(targetPath, forbiddenDep.to, { matchBase: true });
|
|
137
|
+
|
|
138
|
+
return fromMatches && toMatches;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!directViolation) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check if this creates a cycle
|
|
146
|
+
const cycles = this.detectCircularDependencies(nodes, edges);
|
|
147
|
+
|
|
148
|
+
// Check if any cycle involves the forbidden dependency pattern
|
|
149
|
+
return cycles.some(cycle => {
|
|
150
|
+
return cycle.fileCycle.some((filePath, index) => {
|
|
151
|
+
const nextIndex = (index + 1) % cycle.fileCycle.length;
|
|
152
|
+
const nextFilePath = cycle.fileCycle[nextIndex];
|
|
153
|
+
|
|
154
|
+
const fromMatches = patternMatcher.match(filePath, forbiddenDep.from, { matchBase: true });
|
|
155
|
+
const toMatches = patternMatcher.match(nextFilePath, forbiddenDep.to, { matchBase: true });
|
|
156
|
+
|
|
157
|
+
return fromMatches && toMatches;
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Calculate cycle severity based on weight and length
|
|
164
|
+
*/
|
|
165
|
+
calculateCycleSeverity(totalWeight, cycleLength) {
|
|
166
|
+
const baseSeverity = totalWeight / cycleLength;
|
|
167
|
+
if (baseSeverity >= 3) return 'CRITICAL';
|
|
168
|
+
if (baseSeverity >= 2) return 'HIGH';
|
|
169
|
+
if (baseSeverity >= 1) return 'MEDIUM';
|
|
170
|
+
return 'LOW';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Format detailed cycle description
|
|
175
|
+
*/
|
|
176
|
+
formatCycleDescription(cycle, nodePaths) {
|
|
177
|
+
const fileCycle = cycle.cycle.map(nodeId => nodePaths.get(nodeId) || nodeId);
|
|
178
|
+
const cycleString = fileCycle.join(' → ');
|
|
179
|
+
|
|
180
|
+
let description = `Circular dependency detected (${cycle.severity}): ${cycleString}`;
|
|
181
|
+
|
|
182
|
+
if (cycle.totalWeight > cycle.length) {
|
|
183
|
+
description += ` [Weight: ${cycle.totalWeight}, Avg: ${(cycle.totalWeight/cycle.length).toFixed(2)}]`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return description;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Calculate impact score for prioritization
|
|
191
|
+
*/
|
|
192
|
+
calculateImpactScore(cycle, nodePaths) {
|
|
193
|
+
// Factors: cycle length, total weight, file path complexity, severity
|
|
194
|
+
const lengthFactor = Math.log(cycle.length + 1);
|
|
195
|
+
const weightFactor = cycle.totalWeight;
|
|
196
|
+
const severityMultiplier = {
|
|
197
|
+
'CRITICAL': 4,
|
|
198
|
+
'HIGH': 3,
|
|
199
|
+
'MEDIUM': 2,
|
|
200
|
+
'LOW': 1
|
|
201
|
+
}[cycle.severity] || 1;
|
|
202
|
+
|
|
203
|
+
// Path complexity factor (longer paths = higher complexity)
|
|
204
|
+
const pathComplexity = cycle.cycle.reduce((sum, nodeId) => {
|
|
205
|
+
const path = nodePaths.get(nodeId) || '';
|
|
206
|
+
return sum + (path.split('/').length + path.split('\\').length) / 2;
|
|
207
|
+
}, 0) / cycle.length;
|
|
208
|
+
|
|
209
|
+
return (lengthFactor * weightFactor * severityMultiplier * pathComplexity);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get file path for a node ID
|
|
214
|
+
*/
|
|
215
|
+
getNodePath(nodeId, nodes) {
|
|
216
|
+
const node = nodes.find(n => (n.id === nodeId) || (n.name === nodeId) || (n.filePath === nodeId));
|
|
217
|
+
return node ? (node.path || node.filePath || node.name) : nodeId;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check for transitive forbidden dependencies
|
|
222
|
+
*/
|
|
223
|
+
checkTransitiveForbiddenDependencies(forbiddenDep, nodes, edges) {
|
|
224
|
+
const violations = [];
|
|
225
|
+
|
|
226
|
+
// Build adjacency list for path finding
|
|
227
|
+
const adjList = new Map();
|
|
228
|
+
for (const edge of edges) {
|
|
229
|
+
if (!adjList.has(edge.from)) {
|
|
230
|
+
adjList.set(edge.from, []);
|
|
231
|
+
}
|
|
232
|
+
adjList.get(edge.from).push(edge.to);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if there's a path from forbidden 'from' to forbidden 'to' through intermediate nodes
|
|
236
|
+
const nodePaths = new Map();
|
|
237
|
+
nodes.forEach(node => nodePaths.set(node.id, node.path));
|
|
238
|
+
|
|
239
|
+
// Find all nodes that match the 'from' pattern
|
|
240
|
+
const fromNodes = nodes.filter(node =>
|
|
241
|
+
patternMatcher.match(node.path, forbiddenDep.from, { matchBase: true })
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Find all nodes that match the 'to' pattern
|
|
245
|
+
const toNodes = nodes.filter(node =>
|
|
246
|
+
patternMatcher.match(node.path, forbiddenDep.to, { matchBase: true })
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Check for transitive paths
|
|
250
|
+
for (const fromNode of fromNodes) {
|
|
251
|
+
for (const toNode of toNodes) {
|
|
252
|
+
if (fromNode.id !== toNode.id) {
|
|
253
|
+
const hasPath = this.hasPath(adjList, fromNode.id, toNode.id);
|
|
254
|
+
if (hasPath) {
|
|
255
|
+
violations.push({
|
|
256
|
+
type: 'TRANSITIVE_FORBIDDEN',
|
|
257
|
+
from: nodePaths.get(fromNode.id),
|
|
258
|
+
to: nodePaths.get(toNode.id),
|
|
259
|
+
description: `Transitive forbidden dependency path: ${nodePaths.get(fromNode.id)} → ... → ${nodePaths.get(toNode.id)}`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return violations;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check for reverse forbidden dependencies
|
|
271
|
+
*/
|
|
272
|
+
checkReverseForbiddenDependencies(forbiddenDep, nodes, edges) {
|
|
273
|
+
const violations = [];
|
|
274
|
+
|
|
275
|
+
// Check if the reverse dependency exists (to → from)
|
|
276
|
+
for (const edge of edges) {
|
|
277
|
+
const sourcePath = this.getNodePath(edge.from, nodes);
|
|
278
|
+
const targetPath = this.getNodePath(edge.to, nodes);
|
|
279
|
+
|
|
280
|
+
const reverseFromMatches = patternMatcher.match(sourcePath, forbiddenDep.to, { matchBase: true });
|
|
281
|
+
const reverseToMatches = patternMatcher.match(targetPath, forbiddenDep.from, { matchBase: true });
|
|
282
|
+
|
|
283
|
+
if (reverseFromMatches && reverseToMatches) {
|
|
284
|
+
violations.push({
|
|
285
|
+
type: 'REVERSE_FORBIDDEN',
|
|
286
|
+
from: sourcePath,
|
|
287
|
+
to: targetPath,
|
|
288
|
+
description: `Reverse forbidden dependency: ${sourcePath} → ${targetPath} (opposite of allowed direction)`
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return violations;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check if there's a path between two nodes
|
|
298
|
+
*/
|
|
299
|
+
hasPath(adjList, start, end) {
|
|
300
|
+
if (start === end) return false;
|
|
301
|
+
|
|
302
|
+
const visited = new Set();
|
|
303
|
+
const queue = [start];
|
|
304
|
+
|
|
305
|
+
while (queue.length > 0) {
|
|
306
|
+
const current = queue.shift();
|
|
307
|
+
if (current === end) return true;
|
|
308
|
+
|
|
309
|
+
if (visited.has(current)) continue;
|
|
310
|
+
visited.add(current);
|
|
311
|
+
|
|
312
|
+
const neighbors = adjList.get(current) || [];
|
|
313
|
+
for (const neighbor of neighbors) {
|
|
314
|
+
if (!visited.has(neighbor)) {
|
|
315
|
+
queue.push(neighbor);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Calculate overall severity from multiple violations
|
|
325
|
+
*/
|
|
326
|
+
calculateOverallSeverity(violations) {
|
|
327
|
+
if (violations.length === 0) return 'NONE';
|
|
328
|
+
|
|
329
|
+
const severityMap = {
|
|
330
|
+
'DIRECT_FORBIDDEN': 4,
|
|
331
|
+
'CIRCULAR_DEPENDENCY': 3,
|
|
332
|
+
'TRANSITIVE_FORBIDDEN': 2,
|
|
333
|
+
'REVERSE_FORBIDDEN': 1
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const maxSeverity = Math.max(...violations.map(v => severityMap[v.type] || 0));
|
|
337
|
+
|
|
338
|
+
if (maxSeverity >= 4) return 'CRITICAL';
|
|
339
|
+
if (maxSeverity >= 3) return 'HIGH';
|
|
340
|
+
if (maxSeverity >= 2) return 'MEDIUM';
|
|
341
|
+
return 'LOW';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Enhanced forbidden dependency checker that considers bidirectional relationships
|
|
346
|
+
* and transitive dependencies
|
|
347
|
+
*/
|
|
348
|
+
checkEnhancedForbiddenDependency(forbiddenDep, nodes, edges) {
|
|
349
|
+
// Comprehensive check for all types of forbidden dependencies
|
|
350
|
+
const violations = [];
|
|
351
|
+
|
|
352
|
+
// Check 1: Direct forbidden dependency
|
|
353
|
+
const directForbidden = edges.some(edge => {
|
|
354
|
+
const sourcePath = this.getNodePath(edge.from, nodes);
|
|
355
|
+
const targetPath = this.getNodePath(edge.to, nodes);
|
|
356
|
+
|
|
357
|
+
const fromMatches = patternMatcher.match(sourcePath, forbiddenDep.from, { matchBase: true });
|
|
358
|
+
const toMatches = patternMatcher.match(targetPath, forbiddenDep.to, { matchBase: true });
|
|
359
|
+
|
|
360
|
+
if (fromMatches && toMatches) {
|
|
361
|
+
violations.push({
|
|
362
|
+
type: 'DIRECT_FORBIDDEN',
|
|
363
|
+
from: sourcePath,
|
|
364
|
+
to: targetPath,
|
|
365
|
+
description: `Direct forbidden dependency: ${sourcePath} → ${targetPath}`
|
|
366
|
+
});
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Check 2: Circular dependency creation
|
|
373
|
+
const createsCycle = this.checkForbiddenCreatesCycle(forbiddenDep, nodes, edges);
|
|
374
|
+
if (createsCycle) {
|
|
375
|
+
violations.push({
|
|
376
|
+
type: 'CIRCULAR_DEPENDENCY',
|
|
377
|
+
description: `Forbidden dependency creates circular dependency involving: ${forbiddenDep.from} ↔ ${forbiddenDep.to}`
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check 3: Transitive forbidden dependencies
|
|
382
|
+
const transitiveViolations = this.checkTransitiveForbiddenDependencies(forbiddenDep, nodes, edges);
|
|
383
|
+
violations.push(...transitiveViolations);
|
|
384
|
+
|
|
385
|
+
// Check 4: Reverse dependency violations
|
|
386
|
+
const reverseViolations = this.checkReverseForbiddenDependencies(forbiddenDep, nodes, edges);
|
|
387
|
+
violations.push(...reverseViolations);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
directForbidden,
|
|
391
|
+
createsCycle,
|
|
392
|
+
transitiveViolations: transitiveViolations.length > 0,
|
|
393
|
+
reverseViolations: reverseViolations.length > 0,
|
|
394
|
+
violated: violations.length > 0,
|
|
395
|
+
violations,
|
|
396
|
+
description: violations.length > 0 ?
|
|
397
|
+
violations.map(v => v.description).join('; ') :
|
|
398
|
+
null,
|
|
399
|
+
severity: this.calculateOverallSeverity(violations)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = { CircularDependencyDetector };
|