arcvision 0.2.14 → 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,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change Impact Evaluator
|
|
3
|
+
* Connects existing ArcVision data to invariants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Evaluates changes against registered invariants
|
|
8
|
+
*/
|
|
9
|
+
function evaluateChange(input) {
|
|
10
|
+
// Validate input
|
|
11
|
+
if (!input) {
|
|
12
|
+
return createErrorResult('Invalid input: input object is required');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { changedFiles, dependencyGraph, invariants, context } = input;
|
|
16
|
+
|
|
17
|
+
// Validate required parameters with enhanced checks
|
|
18
|
+
if (!Array.isArray(changedFiles)) {
|
|
19
|
+
return createErrorResult('Invalid input: changedFiles must be an array');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!dependencyGraph) {
|
|
23
|
+
return createErrorResult('Invalid input: dependencyGraph is required');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!Array.isArray(invariants)) {
|
|
27
|
+
return createErrorResult('Invalid input: invariants must be an array');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const violations = [];
|
|
32
|
+
|
|
33
|
+
// Check each invariant against the changes
|
|
34
|
+
for (const invariant of invariants) {
|
|
35
|
+
try {
|
|
36
|
+
const matchesScope = matchesInvariantScope(changedFiles, invariant);
|
|
37
|
+
|
|
38
|
+
if (matchesScope) {
|
|
39
|
+
const isViolated = checkInvariantRule(invariant, dependencyGraph, changedFiles);
|
|
40
|
+
|
|
41
|
+
if (isViolated) {
|
|
42
|
+
violations.push(invariant);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (ruleError) {
|
|
46
|
+
console.warn(`Warning: Error evaluating invariant ${invariant?.id || 'unknown'}:`, ruleError.message);
|
|
47
|
+
// Continue with other invariants instead of failing completely
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Determine decision based on violations
|
|
52
|
+
if (violations.some(v => v.severity === 'block')) {
|
|
53
|
+
return {
|
|
54
|
+
decision: 'BLOCKED',
|
|
55
|
+
violations,
|
|
56
|
+
reasons: violations.map(v => v.description || 'Unknown violation'),
|
|
57
|
+
details: calculateImpactDetails(input, violations)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (violations.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
decision: 'RISKY',
|
|
64
|
+
violations,
|
|
65
|
+
reasons: violations.map(v => v.description || 'Unknown violation'),
|
|
66
|
+
details: calculateImpactDetails(input, violations)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
decision: 'ALLOWED',
|
|
72
|
+
violations: [],
|
|
73
|
+
reasons: ['No invariant violations detected'],
|
|
74
|
+
details: calculateImpactDetails(input, violations)
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return createErrorResult(`Evaluation failed: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a standardized error result
|
|
83
|
+
*/
|
|
84
|
+
function createErrorResult(errorMessage) {
|
|
85
|
+
return {
|
|
86
|
+
decision: 'ERROR',
|
|
87
|
+
violations: [],
|
|
88
|
+
reasons: [errorMessage],
|
|
89
|
+
details: {
|
|
90
|
+
affectedNodes: 0,
|
|
91
|
+
blastRadiusImpact: 0,
|
|
92
|
+
authorityCoreChanges: false
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Checks if changed files match the invariant's scope
|
|
99
|
+
*/
|
|
100
|
+
function matchesInvariantScope(changedFiles, invariant) {
|
|
101
|
+
if (!invariant || !invariant.scope) {
|
|
102
|
+
return false; // Invalid invariant, don't match
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const scope = invariant.scope;
|
|
106
|
+
|
|
107
|
+
// If no scope restrictions, always matches
|
|
108
|
+
if (!scope.files && !scope.components && !scope.flows) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check file patterns if specified using robust pattern matcher
|
|
113
|
+
if (scope.files && Array.isArray(scope.files) && scope.files.length > 0) {
|
|
114
|
+
try {
|
|
115
|
+
const { patternMatcher } = require('./pattern-matcher');
|
|
116
|
+
return changedFiles.some(file => {
|
|
117
|
+
if (typeof file !== 'string') return false;
|
|
118
|
+
return scope.files.some(pattern => {
|
|
119
|
+
if (typeof pattern !== 'string') return false;
|
|
120
|
+
try {
|
|
121
|
+
return patternMatcher.match(file, pattern, { matchBase: true });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn(`Warning: Invalid pattern "${pattern}" in invariant ${invariant.id || 'unknown'}:`, error.message);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn(`Warning: Could not process file patterns for invariant ${invariant.id || 'unknown'}:`, error.message);
|
|
130
|
+
return false; // If pattern matching fails, don't match the scope
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check component names if specified
|
|
135
|
+
if (scope.components && Array.isArray(scope.components) && scope.components.length > 0) {
|
|
136
|
+
try {
|
|
137
|
+
return changedFiles.some(file => {
|
|
138
|
+
if (typeof file !== 'string') return false;
|
|
139
|
+
return scope.components.some(component => {
|
|
140
|
+
if (typeof component !== 'string') return false;
|
|
141
|
+
return file.toLowerCase().includes(component.toLowerCase());
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.warn(`Warning: Could not process component patterns for invariant ${invariant.id || 'unknown'}:`, error.message);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check flow patterns if specified
|
|
151
|
+
if (scope.flows && Array.isArray(scope.flows) && scope.flows.length > 0) {
|
|
152
|
+
// Placeholder for flow matching logic
|
|
153
|
+
// This would require more sophisticated analysis of the dependency graph
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Checks if an invariant rule is violated by the changes
|
|
162
|
+
*/
|
|
163
|
+
function checkInvariantRule(invariant, dependencyGraph, changedFiles) {
|
|
164
|
+
if (!invariant || !invariant.rule) {
|
|
165
|
+
return false; // Invalid invariant, no violation
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
switch (invariant.rule.type) {
|
|
170
|
+
case 'dependency':
|
|
171
|
+
return checkDependencyRule(invariant, dependencyGraph, changedFiles);
|
|
172
|
+
case 'flow':
|
|
173
|
+
return checkFlowRule(invariant, dependencyGraph, changedFiles);
|
|
174
|
+
case 'ownership':
|
|
175
|
+
return checkOwnershipRule(invariant, dependencyGraph, changedFiles);
|
|
176
|
+
case 'access_control':
|
|
177
|
+
return checkAccessControlRule(invariant, dependencyGraph, changedFiles);
|
|
178
|
+
case 'data_flow':
|
|
179
|
+
return checkDataFlowRule(invariant, dependencyGraph, changedFiles);
|
|
180
|
+
default:
|
|
181
|
+
// Unknown rule types are treated as non-violating to prevent false positives
|
|
182
|
+
console.warn(`Warning: Unknown rule type "${invariant.rule.type}" in invariant ${invariant.id || 'unknown'}`);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.warn(`Warning: Error checking rule for invariant ${invariant.id || 'unknown'}:`, error.message);
|
|
187
|
+
return false; // If rule check fails, treat as non-violating to avoid blocking changes unnecessarily
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generic rule checker that interprets the condition dynamically
|
|
193
|
+
*/
|
|
194
|
+
function checkGenericRule(condition, dependencyGraph, changedFiles) {
|
|
195
|
+
if (!condition) return false;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// Handle different condition types dynamically
|
|
199
|
+
if (condition.forbiddenDependency && typeof condition.forbiddenDependency === 'object') {
|
|
200
|
+
return checkForbiddenDependency(condition.forbiddenDependency, dependencyGraph, changedFiles);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (condition.mustExist) {
|
|
204
|
+
return checkRequirement(condition.mustExist, dependencyGraph, changedFiles);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (condition.mustNotExist) {
|
|
208
|
+
return checkProhibition(condition.mustNotExist, dependencyGraph, changedFiles);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (condition.pattern) {
|
|
212
|
+
return checkPatternMatch(condition.pattern, dependencyGraph, changedFiles);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Default: if condition exists and has properties, consider it potentially violated
|
|
216
|
+
return typeof condition === 'object' && Object.keys(condition).length > 0;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.warn(`Warning: Error in generic rule check:`, error.message);
|
|
219
|
+
return false; // If generic check fails, don't treat as violation
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check dependency rule violations
|
|
225
|
+
*/
|
|
226
|
+
function checkDependencyRule(invariant, dependencyGraph, changedFiles) {
|
|
227
|
+
// Use the generic rule checker to interpret the condition
|
|
228
|
+
return checkGenericRule(invariant.rule.condition, dependencyGraph, changedFiles);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check forbidden dependency specifically - more generic approach
|
|
233
|
+
*/
|
|
234
|
+
function checkForbiddenDependency(forbiddenDep, dependencyGraph, changedFiles) {
|
|
235
|
+
if (!forbiddenDep || typeof forbiddenDep !== 'object') {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!forbiddenDep.from || !forbiddenDep.to) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Check if any changed file matches the 'from' pattern and creates dependency to 'to'
|
|
245
|
+
const { patternMatcher } = require('./pattern-matcher');
|
|
246
|
+
|
|
247
|
+
return changedFiles.some(file => {
|
|
248
|
+
if (typeof file !== 'string') return false;
|
|
249
|
+
|
|
250
|
+
// Check if file matches the 'from' scope using robust pattern matching
|
|
251
|
+
const matchesFrom = patternMatcher.match(file, forbiddenDep.from, { matchBase: true }) ||
|
|
252
|
+
patternMatcher.match(file, `**/${forbiddenDep.from}/**`, { matchBase: false }) ||
|
|
253
|
+
file.toLowerCase().includes(forbiddenDep.from.toLowerCase());
|
|
254
|
+
|
|
255
|
+
if (matchesFrom) {
|
|
256
|
+
// Check if this file now depends on the 'to' component
|
|
257
|
+
return hasDependencyTo(dependencyGraph, file, forbiddenDep.to);
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.warn(`Warning: Error checking forbidden dependency:`, error.message);
|
|
263
|
+
return false; // If dependency check fails, don't block
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check general requirement
|
|
269
|
+
*/
|
|
270
|
+
function checkRequirement(requirement, dependencyGraph, changedFiles) {
|
|
271
|
+
// Placeholder for requirement checking logic
|
|
272
|
+
// For now, return false to avoid false positives
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check prohibition
|
|
278
|
+
*/
|
|
279
|
+
function checkProhibition(prohibition, dependencyGraph, changedFiles) {
|
|
280
|
+
// Placeholder for prohibition checking logic
|
|
281
|
+
// For now, return false to avoid false positives
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check pattern matching
|
|
287
|
+
*/
|
|
288
|
+
function checkPatternMatch(pattern, dependencyGraph, changedFiles) {
|
|
289
|
+
// Placeholder for pattern matching logic
|
|
290
|
+
// For now, return false to avoid false positives
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check flow rule violations
|
|
296
|
+
*/
|
|
297
|
+
function checkFlowRule(invariant, dependencyGraph, changedFiles) {
|
|
298
|
+
// Use the generic rule checker to interpret the condition
|
|
299
|
+
return checkGenericRule(invariant.rule.condition, dependencyGraph, changedFiles);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check ownership rule violations
|
|
304
|
+
*/
|
|
305
|
+
function checkOwnershipRule(invariant, dependencyGraph, changedFiles) {
|
|
306
|
+
try {
|
|
307
|
+
// Check if changed files fall under this invariant's scope and if ownership is violated
|
|
308
|
+
if (invariant.rule.condition && invariant.scope.files) {
|
|
309
|
+
const { authorizedOwners } = invariant.rule.condition;
|
|
310
|
+
|
|
311
|
+
if (authorizedOwners && Array.isArray(authorizedOwners)) {
|
|
312
|
+
return changedFiles.some(file => {
|
|
313
|
+
if (typeof file !== 'string') return false;
|
|
314
|
+
|
|
315
|
+
// Check if file matches the scope
|
|
316
|
+
const matchesScope = invariant.scope.files.some(pattern => {
|
|
317
|
+
if (typeof pattern !== 'string') return false;
|
|
318
|
+
try {
|
|
319
|
+
const { patternMatcher } = require('./pattern-matcher');
|
|
320
|
+
return patternMatcher.match(file, pattern, { matchBase: true });
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn(`Warning: Invalid pattern "${pattern}" in ownership check:`, error.message);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (matchesScope) {
|
|
328
|
+
// Check if current owner is authorized (this would require additional context about who made the change)
|
|
329
|
+
// For now, we'll just check if the invariant has an owner specified and it's different
|
|
330
|
+
return invariant.owner && !authorizedOwners.includes(invariant.owner);
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.warn(`Warning: Error in ownership rule check:`, error.message);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Check access control rule violations
|
|
346
|
+
*/
|
|
347
|
+
function checkAccessControlRule(invariant, dependencyGraph, changedFiles) {
|
|
348
|
+
// Use the generic rule checker to interpret the condition
|
|
349
|
+
return checkGenericRule(invariant.rule.condition, dependencyGraph, changedFiles);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check data flow rule violations
|
|
354
|
+
*/
|
|
355
|
+
function checkDataFlowRule(invariant, dependencyGraph, changedFiles) {
|
|
356
|
+
// Use the generic rule checker to interpret the condition
|
|
357
|
+
return checkGenericRule(invariant.rule.condition, dependencyGraph, changedFiles);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Helper function to check if a file has dependency to another component
|
|
362
|
+
*/
|
|
363
|
+
function hasDependencyTo(dependencyGraph, fromFile, toComponent) {
|
|
364
|
+
if (typeof fromFile !== 'string' || typeof toComponent !== 'string') {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
// Simplified implementation - would need to analyze actual dependency graph
|
|
370
|
+
if (dependencyGraph && dependencyGraph.edges && Array.isArray(dependencyGraph.edges)) {
|
|
371
|
+
return dependencyGraph.edges.some((edge) => {
|
|
372
|
+
if (!edge || !edge.source || !edge.target) return false;
|
|
373
|
+
|
|
374
|
+
return edge.source === fromFile &&
|
|
375
|
+
(edge.target.includes(toComponent) ||
|
|
376
|
+
edge.target.toLowerCase().includes(toComponent.toLowerCase()));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.warn(`Warning: Error checking dependency graph:`, error.message);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Calculate impact details for the evaluation result
|
|
388
|
+
*/
|
|
389
|
+
function calculateImpactDetails(input, violations) {
|
|
390
|
+
const { changedFiles, dependencyGraph, context } = input || {};
|
|
391
|
+
|
|
392
|
+
// Calculate affected nodes from dependency graph
|
|
393
|
+
let affectedNodes = 0;
|
|
394
|
+
if (dependencyGraph && dependencyGraph.nodes && Array.isArray(dependencyGraph.nodes)) {
|
|
395
|
+
affectedNodes = dependencyGraph.nodes.filter((node) =>
|
|
396
|
+
changedFiles && Array.isArray(changedFiles) &&
|
|
397
|
+
changedFiles.includes(node.id || node.path)
|
|
398
|
+
).length;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Calculate blast radius impact if available in context
|
|
402
|
+
let blastRadiusImpact = 0;
|
|
403
|
+
if (context && context.blastRadiusAnalysis) {
|
|
404
|
+
blastRadiusImpact = context.blastRadiusAnalysis.totalAffected || 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Check for authority core changes
|
|
408
|
+
let authorityCoreChanges = false;
|
|
409
|
+
if (context && Array.isArray(context.authorityCores)) {
|
|
410
|
+
authorityCoreChanges = changedFiles && Array.isArray(changedFiles) &&
|
|
411
|
+
changedFiles.some(file =>
|
|
412
|
+
context.authorityCores.some((core) => core.path === file)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
affectedNodes,
|
|
418
|
+
blastRadiusImpact,
|
|
419
|
+
authorityCoreChanges
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = { evaluateChange };function isValidInvariant(invariant) { if (!invariant || typeof invariant !== 'object') return false; if (!invariant.id || typeof invariant.id !== 'string' || invariant.id.trim().length === 0) return false; if (!invariant.system || typeof invariant.system !== 'string' || invariant.system.trim().length === 0) return false; if (!invariant.description || typeof invariant.description !== 'string' || invariant.description.trim().length === 0) return false; if (!invariant.severity || typeof invariant.severity !== 'string' || !['block', 'risk', 'BLOCK', 'RISK'].includes(invariant.severity)) return false; if (!invariant.scope || typeof invariant.scope !== 'object') return false; if (!invariant.rule || typeof invariant.rule !== 'object') return false; return true; }
|
|
424
|
+
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Invariant } from './invariant-registry';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change Impact Evaluator
|
|
5
|
+
* Connects existing ArcVision data to invariants
|
|
6
|
+
*/
|
|
7
|
+
export interface ChangeEvaluationInput {
|
|
8
|
+
changedFiles: string[];
|
|
9
|
+
dependencyGraph: any; // ArcVision's existing graph structure
|
|
10
|
+
invariants: Invariant[];
|
|
11
|
+
context?: any; // Additional context from ArcVision analysis
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChangeEvaluationResult {
|
|
15
|
+
decision: 'ALLOWED' | 'RISKY' | 'BLOCKED';
|
|
16
|
+
violations: Invariant[];
|
|
17
|
+
reasons: string[];
|
|
18
|
+
details: {
|
|
19
|
+
affectedNodes: number;
|
|
20
|
+
blastRadiusImpact: number;
|
|
21
|
+
authorityCoreChanges: boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Evaluates changes against registered invariants
|
|
27
|
+
*/
|
|
28
|
+
export function evaluateChange(input: ChangeEvaluationInput): ChangeEvaluationResult {
|
|
29
|
+
const { changedFiles, dependencyGraph, invariants } = input;
|
|
30
|
+
const violations: Invariant[] = [];
|
|
31
|
+
|
|
32
|
+
// Check each invariant against the changes
|
|
33
|
+
for (const invariant of invariants) {
|
|
34
|
+
const matchesScope = matchesInvariantScope(changedFiles, invariant);
|
|
35
|
+
|
|
36
|
+
if (matchesScope) {
|
|
37
|
+
const isViolated = checkInvariantRule(invariant, dependencyGraph, changedFiles);
|
|
38
|
+
|
|
39
|
+
if (isViolated) {
|
|
40
|
+
violations.push(invariant);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Determine decision based on violations
|
|
46
|
+
if (violations.some(v => v.severity === 'block')) {
|
|
47
|
+
return {
|
|
48
|
+
decision: 'BLOCKED',
|
|
49
|
+
violations,
|
|
50
|
+
reasons: violations.map(v => v.description),
|
|
51
|
+
details: calculateImpactDetails(input, violations)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (violations.length > 0) {
|
|
56
|
+
return {
|
|
57
|
+
decision: 'RISKY',
|
|
58
|
+
violations,
|
|
59
|
+
reasons: violations.map(v => v.description),
|
|
60
|
+
details: calculateImpactDetails(input, violations)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
decision: 'ALLOWED',
|
|
66
|
+
violations: [],
|
|
67
|
+
reasons: ['No invariant violations detected'],
|
|
68
|
+
details: calculateImpactDetails(input, violations)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checks if changed files match the invariant's scope
|
|
74
|
+
*/
|
|
75
|
+
function matchesInvariantScope(changedFiles: string[], invariant: Invariant): boolean {
|
|
76
|
+
const scope = invariant.scope;
|
|
77
|
+
|
|
78
|
+
// If no scope restrictions, always matches
|
|
79
|
+
if (!scope.files && !scope.components && !scope.flows) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check file patterns if specified
|
|
84
|
+
if (scope.files && scope.files.length > 0) {
|
|
85
|
+
const minimatch = require('minimatch');
|
|
86
|
+
return changedFiles.some(file =>
|
|
87
|
+
scope.files!.some(pattern =>
|
|
88
|
+
minimatch(file, pattern, { matchBase: true })
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check component names if specified
|
|
94
|
+
if (scope.components && scope.components.length > 0) {
|
|
95
|
+
// For now, check if any changed file contains a component name
|
|
96
|
+
return changedFiles.some(file =>
|
|
97
|
+
scope.components!.some(component =>
|
|
98
|
+
file.toLowerCase().includes(component.toLowerCase())
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check flow patterns if specified
|
|
104
|
+
if (scope.flows && scope.flows.length > 0) {
|
|
105
|
+
// Placeholder for flow matching logic
|
|
106
|
+
// This would require more sophisticated analysis of the dependency graph
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Checks if an invariant rule is violated by the changes
|
|
115
|
+
*/
|
|
116
|
+
function checkInvariantRule(
|
|
117
|
+
invariant: Invariant,
|
|
118
|
+
dependencyGraph: any,
|
|
119
|
+
changedFiles: string[]
|
|
120
|
+
): boolean {
|
|
121
|
+
switch (invariant.rule.type) {
|
|
122
|
+
case 'dependency':
|
|
123
|
+
return checkDependencyRule(invariant, dependencyGraph, changedFiles);
|
|
124
|
+
case 'flow':
|
|
125
|
+
return checkFlowRule(invariant, dependencyGraph, changedFiles);
|
|
126
|
+
case 'ownership':
|
|
127
|
+
return checkOwnershipRule(invariant, dependencyGraph, changedFiles);
|
|
128
|
+
case 'access_control':
|
|
129
|
+
return checkAccessControlRule(invariant, dependencyGraph, changedFiles);
|
|
130
|
+
case 'data_flow':
|
|
131
|
+
return checkDataFlowRule(invariant, dependencyGraph, changedFiles);
|
|
132
|
+
default:
|
|
133
|
+
console.warn(`Unknown rule type: ${invariant.rule.type}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check dependency rule violations
|
|
140
|
+
*/
|
|
141
|
+
function checkDependencyRule(
|
|
142
|
+
invariant: Invariant,
|
|
143
|
+
dependencyGraph: any,
|
|
144
|
+
changedFiles: string[]
|
|
145
|
+
): boolean {
|
|
146
|
+
// Example: "Auth service cannot depend on user service"
|
|
147
|
+
// Check if the dependency graph now contains a dependency that violates the rule
|
|
148
|
+
if (invariant.rule.condition && typeof invariant.rule.condition === 'object') {
|
|
149
|
+
const { forbiddenDependency } = invariant.rule.condition;
|
|
150
|
+
|
|
151
|
+
if (forbiddenDependency && typeof forbiddenDependency === 'object') {
|
|
152
|
+
const { from, to } = forbiddenDependency;
|
|
153
|
+
|
|
154
|
+
// Check if 'from' component now depends on 'to' component
|
|
155
|
+
// This is a simplified check - real implementation would traverse the dependency graph
|
|
156
|
+
if (from && to) {
|
|
157
|
+
// Check if any changed file is in the 'from' scope and creates dependency to 'to'
|
|
158
|
+
return changedFiles.some(file => {
|
|
159
|
+
// Simplified logic - in reality would need to analyze the dependency graph
|
|
160
|
+
return file.includes(from) && hasDependencyTo(dependencyGraph, file, to);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check flow rule violations
|
|
171
|
+
*/
|
|
172
|
+
function checkFlowRule(
|
|
173
|
+
invariant: Invariant,
|
|
174
|
+
dependencyGraph: any,
|
|
175
|
+
changedFiles: string[]
|
|
176
|
+
): boolean {
|
|
177
|
+
// Example: "Payments flow must never bypass fraud check"
|
|
178
|
+
// This would require flow analysis of the code changes
|
|
179
|
+
return false; // Placeholder
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check ownership rule violations
|
|
184
|
+
*/
|
|
185
|
+
function checkOwnershipRule(
|
|
186
|
+
invariant: Invariant,
|
|
187
|
+
dependencyGraph: any,
|
|
188
|
+
changedFiles: string[]
|
|
189
|
+
): boolean {
|
|
190
|
+
// Example: "Only billing team can modify these files"
|
|
191
|
+
// Check if changed files are owned by unauthorized team
|
|
192
|
+
if (invariant.rule.condition && invariant.scope.files) {
|
|
193
|
+
const { authorizedOwners } = invariant.rule.condition;
|
|
194
|
+
|
|
195
|
+
if (authorizedOwners && Array.isArray(authorizedOwners)) {
|
|
196
|
+
// Check if any changed file is restricted to specific owners
|
|
197
|
+
return changedFiles.some(file => {
|
|
198
|
+
// If file matches invariant scope and owner is not authorized
|
|
199
|
+
return invariant.scope.files?.some(pattern => {
|
|
200
|
+
const minimatch = require('minimatch');
|
|
201
|
+
return minimatch(file, pattern, { matchBase: true });
|
|
202
|
+
}) && !authorizedOwners.includes(invariant.owner || '');
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check access control rule violations
|
|
212
|
+
*/
|
|
213
|
+
function checkAccessControlRule(
|
|
214
|
+
invariant: Invariant,
|
|
215
|
+
dependencyGraph: any,
|
|
216
|
+
changedFiles: string[]
|
|
217
|
+
): boolean {
|
|
218
|
+
// Example: "Direct database access not allowed from controllers"
|
|
219
|
+
// Check if changes create unauthorized access patterns
|
|
220
|
+
return false; // Placeholder
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check data flow rule violations
|
|
225
|
+
*/
|
|
226
|
+
function checkDataFlowRule(
|
|
227
|
+
invariant: Invariant,
|
|
228
|
+
dependencyGraph: any,
|
|
229
|
+
changedFiles: string[]
|
|
230
|
+
): boolean {
|
|
231
|
+
// Example: "PII data must not flow to logging systems"
|
|
232
|
+
// Check data flow patterns in the changes
|
|
233
|
+
return false; // Placeholder
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Helper function to check if a file has dependency to another component
|
|
238
|
+
*/
|
|
239
|
+
function hasDependencyTo(dependencyGraph: any, fromFile: string, toComponent: string): boolean {
|
|
240
|
+
// Simplified implementation - would need to analyze actual dependency graph
|
|
241
|
+
if (dependencyGraph && dependencyGraph.edges) {
|
|
242
|
+
return dependencyGraph.edges.some((edge: any) => {
|
|
243
|
+
return edge.source === fromFile && edge.target.includes(toComponent);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Calculate impact details for the evaluation result
|
|
251
|
+
*/
|
|
252
|
+
function calculateImpactDetails(
|
|
253
|
+
input: ChangeEvaluationInput,
|
|
254
|
+
violations: Invariant[]
|
|
255
|
+
): ChangeEvaluationResult['details'] {
|
|
256
|
+
const { changedFiles, dependencyGraph, context } = input;
|
|
257
|
+
|
|
258
|
+
// Calculate affected nodes from dependency graph
|
|
259
|
+
let affectedNodes = 0;
|
|
260
|
+
if (dependencyGraph && dependencyGraph.nodes) {
|
|
261
|
+
affectedNodes = dependencyGraph.nodes.filter((node: any) =>
|
|
262
|
+
changedFiles.includes(node.id || node.path)
|
|
263
|
+
).length;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Calculate blast radius impact if available in context
|
|
267
|
+
let blastRadiusImpact = 0;
|
|
268
|
+
if (context && context.blastRadiusAnalysis) {
|
|
269
|
+
blastRadiusImpact = context.blastRadiusAnalysis.totalAffected || 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check for authority core changes
|
|
273
|
+
let authorityCoreChanges = false;
|
|
274
|
+
if (context && context.authorityCores) {
|
|
275
|
+
authorityCoreChanges = changedFiles.some(file =>
|
|
276
|
+
context.authorityCores.some((core: any) => core.path === file)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
affectedNodes,
|
|
282
|
+
blastRadiusImpact,
|
|
283
|
+
authorityCoreChanges
|
|
284
|
+
};
|
|
285
|
+
}
|