arcvision 0.2.25 ā 0.2.26
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/README.md +59 -0
- package/package.json +1 -1
- package/src/core/authority-ledger.js +18 -20
- package/src/core/change-evaluator.js +56 -53
- package/src/core/cli-validator.js +59 -61
- package/src/core/invariant-detector.js +123 -103
- package/src/core/override-handler.js +28 -23
- package/src/core/scanner.js +114 -74
- package/src/engine/pass4_signals.js +12 -40
- package/src/index.js +38 -91
|
@@ -21,17 +21,17 @@ class InvariantDetector {
|
|
|
21
21
|
*/
|
|
22
22
|
async detectInvariants(scanResult, directory) {
|
|
23
23
|
this.detectedInvariants = [];
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
try {
|
|
26
26
|
// Analyze the structural context for potential invariants
|
|
27
27
|
await this.analyzeNodesForInvariants(scanResult.nodes, directory);
|
|
28
28
|
await this.analyzeEdgesForInvariants(scanResult.edges);
|
|
29
29
|
await this.analyzeCodeFilesForInvariants(directory);
|
|
30
30
|
await this.analyzeStructuralPatterns(scanResult.nodes, scanResult.edges, directory);
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
console.log(`\nš”ļø Invariant Detection Complete`);
|
|
33
33
|
console.log(` Found ${this.detectedInvariants.length} potential system invariants`);
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
return this.detectedInvariants;
|
|
36
36
|
} catch (error) {
|
|
37
37
|
console.error('Invariant detection failed:', error.message);
|
|
@@ -44,20 +44,24 @@ class InvariantDetector {
|
|
|
44
44
|
*/
|
|
45
45
|
async analyzeNodesForInvariants(nodes, directory) {
|
|
46
46
|
for (const node of nodes) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const nodePath = node.path || node.id;
|
|
48
|
+
if (!nodePath) continue;
|
|
49
|
+
|
|
49
50
|
try {
|
|
50
|
-
const filePath = path.join(directory,
|
|
51
|
+
const filePath = path.join(directory, nodePath);
|
|
51
52
|
if (await this.isFileAccessible(filePath)) {
|
|
52
53
|
const content = await fs.readFile(filePath, 'utf8');
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
// Look for validation/assertion patterns in the file
|
|
55
|
-
const fileInvariants = this.extractInvariantsFromFile(content,
|
|
56
|
-
|
|
56
|
+
const fileInvariants = this.extractInvariantsFromFile(content, nodePath);
|
|
57
|
+
|
|
57
58
|
for (const invariant of fileInvariants) {
|
|
58
|
-
const nodeId =
|
|
59
|
+
const nodeId = nodePath || 'unknown';
|
|
60
|
+
// Use position in ID to avoid collisions for multiple instances in same file
|
|
61
|
+
const invariantId = this.generateInvariantId(invariant.pattern, nodeId, invariant.position);
|
|
62
|
+
|
|
59
63
|
this.detectedInvariants.push({
|
|
60
|
-
id:
|
|
64
|
+
id: invariantId,
|
|
61
65
|
system: 'automatic',
|
|
62
66
|
statement: invariant.description,
|
|
63
67
|
description: invariant.description,
|
|
@@ -74,6 +78,7 @@ class InvariantDetector {
|
|
|
74
78
|
condition: { pattern: invariant.pattern }
|
|
75
79
|
},
|
|
76
80
|
source_file: nodeId,
|
|
81
|
+
position: invariant.position,
|
|
77
82
|
detected_at: new Date().toISOString()
|
|
78
83
|
});
|
|
79
84
|
}
|
|
@@ -91,7 +96,7 @@ class InvariantDetector {
|
|
|
91
96
|
async analyzeEdgesForInvariants(edges) {
|
|
92
97
|
// Look for dependency patterns that suggest invariants
|
|
93
98
|
const dependencyConstraints = this.identifyDependencyInvariants(edges);
|
|
94
|
-
|
|
99
|
+
|
|
95
100
|
for (const constraint of dependencyConstraints) {
|
|
96
101
|
this.detectedInvariants.push({
|
|
97
102
|
id: this.generateInvariantId('dependency_constraint', constraint.from + '_' + constraint.to),
|
|
@@ -126,18 +131,19 @@ class InvariantDetector {
|
|
|
126
131
|
*/
|
|
127
132
|
async analyzeCodeFilesForInvariants(directory) {
|
|
128
133
|
const invariantFiles = await this.findInvariantRelatedFiles(directory);
|
|
129
|
-
|
|
134
|
+
|
|
130
135
|
for (const filePath of invariantFiles) {
|
|
131
136
|
try {
|
|
132
137
|
const content = await fs.readFile(filePath, 'utf8');
|
|
133
138
|
const relativePath = path.relative(directory, filePath);
|
|
134
|
-
|
|
139
|
+
|
|
135
140
|
// Look for common invariant implementation patterns
|
|
136
141
|
const patterns = this.searchForInvariantPatterns(content);
|
|
137
|
-
|
|
138
|
-
for (
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
144
|
+
const pattern = patterns[i];
|
|
139
145
|
this.detectedInvariants.push({
|
|
140
|
-
id: this.generateInvariantId(pattern.type, relativePath),
|
|
146
|
+
id: this.generateInvariantId(pattern.type, relativePath, i),
|
|
141
147
|
system: 'automatic',
|
|
142
148
|
statement: `System invariant implemented in ${relativePath}: ${pattern.description}`,
|
|
143
149
|
description: `System invariant implemented in ${relativePath}: ${pattern.description}`,
|
|
@@ -170,7 +176,7 @@ class InvariantDetector {
|
|
|
170
176
|
*/
|
|
171
177
|
extractInvariantsFromFile(content, filePath) {
|
|
172
178
|
const invariants = [];
|
|
173
|
-
|
|
179
|
+
|
|
174
180
|
// Check for validation function patterns
|
|
175
181
|
const validationPatterns = [
|
|
176
182
|
/function\s+validate\w+/gi,
|
|
@@ -181,7 +187,7 @@ class InvariantDetector {
|
|
|
181
187
|
/function\s+assert\w+/gi,
|
|
182
188
|
/function\s+enforce\w+/gi
|
|
183
189
|
];
|
|
184
|
-
|
|
190
|
+
|
|
185
191
|
for (const pattern of validationPatterns) {
|
|
186
192
|
const matches = content.match(pattern);
|
|
187
193
|
if (matches) {
|
|
@@ -192,7 +198,7 @@ class InvariantDetector {
|
|
|
192
198
|
});
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
|
-
|
|
201
|
+
|
|
196
202
|
// Check for error handling patterns that suggest invariant enforcement
|
|
197
203
|
const errorHandlingPatterns = [
|
|
198
204
|
/try\s*\{/gi,
|
|
@@ -201,7 +207,7 @@ class InvariantDetector {
|
|
|
201
207
|
/if\s*\([^)]+\)\s*throw/gi,
|
|
202
208
|
/error\.message/gi
|
|
203
209
|
];
|
|
204
|
-
|
|
210
|
+
|
|
205
211
|
for (const pattern of errorHandlingPatterns) {
|
|
206
212
|
const match = content.match(pattern);
|
|
207
213
|
if (match) {
|
|
@@ -212,7 +218,7 @@ class InvariantDetector {
|
|
|
212
218
|
});
|
|
213
219
|
}
|
|
214
220
|
}
|
|
215
|
-
|
|
221
|
+
|
|
216
222
|
// Check for state management patterns
|
|
217
223
|
const stateManagementPatterns = [
|
|
218
224
|
/setState\s*\(/gi,
|
|
@@ -223,7 +229,7 @@ class InvariantDetector {
|
|
|
223
229
|
/state\./gi,
|
|
224
230
|
/state\s*=\s*/gi
|
|
225
231
|
];
|
|
226
|
-
|
|
232
|
+
|
|
227
233
|
for (const pattern of stateManagementPatterns) {
|
|
228
234
|
const match = content.match(pattern);
|
|
229
235
|
if (match) {
|
|
@@ -234,7 +240,7 @@ class InvariantDetector {
|
|
|
234
240
|
});
|
|
235
241
|
}
|
|
236
242
|
}
|
|
237
|
-
|
|
243
|
+
|
|
238
244
|
// Check for boundary enforcement patterns
|
|
239
245
|
const boundaryCheckPatterns = [
|
|
240
246
|
/boundary\s*check/gi,
|
|
@@ -246,7 +252,7 @@ class InvariantDetector {
|
|
|
246
252
|
/validate\s*input/gi,
|
|
247
253
|
/sanitiz/gi
|
|
248
254
|
];
|
|
249
|
-
|
|
255
|
+
|
|
250
256
|
for (const pattern of boundaryCheckPatterns) {
|
|
251
257
|
const match = content.match(pattern);
|
|
252
258
|
if (match) {
|
|
@@ -257,22 +263,22 @@ class InvariantDetector {
|
|
|
257
263
|
});
|
|
258
264
|
}
|
|
259
265
|
}
|
|
260
|
-
|
|
266
|
+
|
|
261
267
|
// Check for invariant indicators from pass 1
|
|
262
268
|
const invariantIndicators = this.extractDetailedInvariantIndicators(content, filePath);
|
|
263
269
|
for (const indicator of invariantIndicators) {
|
|
264
270
|
invariants.push(indicator);
|
|
265
271
|
}
|
|
266
|
-
|
|
272
|
+
|
|
267
273
|
return invariants;
|
|
268
274
|
}
|
|
269
|
-
|
|
275
|
+
|
|
270
276
|
/**
|
|
271
277
|
* Extract detailed invariant indicators from content
|
|
272
278
|
*/
|
|
273
279
|
extractDetailedInvariantIndicators(content, filePath) {
|
|
274
280
|
const indicators = [];
|
|
275
|
-
|
|
281
|
+
|
|
276
282
|
// Look for common invariant/pattern validation patterns
|
|
277
283
|
const patterns = [
|
|
278
284
|
// Assertion and validation patterns
|
|
@@ -284,13 +290,14 @@ class InvariantDetector {
|
|
|
284
290
|
{ pattern: /guard[A-Z]/gi, description: 'Guard function pattern', critical: true },
|
|
285
291
|
{ pattern: /verify[A-Z]/gi, description: 'Verify function pattern', critical: false },
|
|
286
292
|
{ pattern: /validate\(/gi, description: 'Validation function call', critical: false },
|
|
287
|
-
|
|
293
|
+
|
|
288
294
|
// Constraint and invariant mentions
|
|
289
295
|
{ pattern: /Invariant/gi, description: 'Invariant mention', critical: true },
|
|
290
296
|
{ pattern: /constraint/gi, description: 'Constraint mention', critical: true },
|
|
291
|
-
{ pattern: /
|
|
297
|
+
{ pattern: /VIOLATION/gi, description: 'Explicit violation marker', critical: true },
|
|
298
|
+
{ pattern: /BLOCKED/gi, description: 'Blocking architectural pattern', critical: true },
|
|
292
299
|
{ pattern: /enforce/gi, description: 'Enforcement pattern', critical: true },
|
|
293
|
-
|
|
300
|
+
|
|
294
301
|
// Conditional guard patterns
|
|
295
302
|
{ pattern: /must.*be/gi, description: 'Must-be pattern', critical: true },
|
|
296
303
|
{ pattern: /should.*be/gi, description: 'Should-be pattern', critical: false },
|
|
@@ -298,39 +305,39 @@ class InvariantDetector {
|
|
|
298
305
|
{ pattern: /if.*!\s*[^\s]+\s*throw/gi, description: 'Guard clause pattern', critical: true },
|
|
299
306
|
{ pattern: /if.*!\s*[^\s]+\s*reject/gi, description: 'Promise rejection guard', critical: true },
|
|
300
307
|
{ pattern: /throw.*Error/gi, description: 'Error throwing pattern', critical: true },
|
|
301
|
-
|
|
308
|
+
|
|
302
309
|
// Boundary and validation patterns
|
|
303
310
|
{ pattern: /boundary.*check/gi, description: 'Boundary check', critical: true },
|
|
304
311
|
{ pattern: /range.*check/gi, description: 'Range check', critical: true },
|
|
305
312
|
{ pattern: /within.*bounds/gi, description: 'Bounds check', critical: true },
|
|
306
313
|
{ pattern: /validat(e|ion)/gi, description: 'Validation pattern', critical: false },
|
|
307
314
|
{ pattern: /guard/i, description: 'Guard pattern', critical: true },
|
|
308
|
-
|
|
315
|
+
|
|
309
316
|
// Access control and security patterns
|
|
310
317
|
{ pattern: /access.*control/gi, description: 'Access control', critical: true },
|
|
311
318
|
{ pattern: /permission.*check/gi, description: 'Permission check', critical: true },
|
|
312
319
|
{ pattern: /auth(?:oriz|enticat)/gi, description: 'Authorization/authentication', critical: true },
|
|
313
320
|
{ pattern: /acl/gi, description: 'Access Control List', critical: true },
|
|
314
|
-
|
|
321
|
+
|
|
315
322
|
// State management patterns
|
|
316
323
|
{ pattern: /state.*transition/gi, description: 'State transition', critical: true },
|
|
317
324
|
{ pattern: /setState/gi, description: 'State setter', critical: true },
|
|
318
325
|
{ pattern: /update.*state/gi, description: 'State updater', critical: true },
|
|
319
326
|
{ pattern: /transition/gi, description: 'Transition pattern', critical: true },
|
|
320
327
|
{ pattern: /immutable/gi, description: 'Immutability pattern', critical: true },
|
|
321
|
-
|
|
328
|
+
|
|
322
329
|
// Type and schema validation patterns
|
|
323
330
|
{ pattern: /zod/gi, description: 'Zod validation schema', critical: false },
|
|
324
331
|
{ pattern: /joi/gi, description: 'Joi validation schema', critical: false },
|
|
325
332
|
{ pattern: /yup/gi, description: 'Yup validation schema', critical: false },
|
|
326
333
|
{ pattern: /ajv/gi, description: 'Ajv validation schema', critical: false },
|
|
327
334
|
{ pattern: /schema/gi, description: 'Schema definition', critical: false },
|
|
328
|
-
|
|
335
|
+
|
|
329
336
|
// Configuration patterns
|
|
330
337
|
{ pattern: /config.*environ/gi, description: 'Environment configuration', critical: true },
|
|
331
338
|
{ pattern: /env.*validation/gi, description: 'Environment validation', critical: true }
|
|
332
339
|
];
|
|
333
|
-
|
|
340
|
+
|
|
334
341
|
for (const item of patterns) {
|
|
335
342
|
if (!item.pattern || !content) continue; // Skip if pattern or content is undefined
|
|
336
343
|
let match;
|
|
@@ -338,13 +345,14 @@ class InvariantDetector {
|
|
|
338
345
|
while ((match = item.pattern.exec(content)) !== null) {
|
|
339
346
|
// Avoid duplicate matches by position
|
|
340
347
|
const position = item.pattern.lastIndex;
|
|
341
|
-
const prevMatch = indicators.find(ind => ind.pattern === item.pattern.source &&
|
|
342
|
-
|
|
348
|
+
const prevMatch = indicators.find(ind => ind.pattern === item.pattern.source &&
|
|
349
|
+
ind.description.includes(match[0]));
|
|
343
350
|
if (!prevMatch) {
|
|
344
351
|
indicators.push({
|
|
345
352
|
pattern: item.pattern.source,
|
|
346
353
|
description: `${item.description} found in ${filePath}: '${match[0]}' at position ${position}`,
|
|
347
|
-
isCritical: item.critical
|
|
354
|
+
isCritical: item.critical,
|
|
355
|
+
position: position
|
|
348
356
|
});
|
|
349
357
|
}
|
|
350
358
|
}
|
|
@@ -353,7 +361,7 @@ class InvariantDetector {
|
|
|
353
361
|
continue;
|
|
354
362
|
}
|
|
355
363
|
}
|
|
356
|
-
|
|
364
|
+
|
|
357
365
|
return indicators;
|
|
358
366
|
}
|
|
359
367
|
|
|
@@ -362,11 +370,11 @@ class InvariantDetector {
|
|
|
362
370
|
*/
|
|
363
371
|
identifyDependencyInvariants(edges) {
|
|
364
372
|
const constraints = [];
|
|
365
|
-
|
|
373
|
+
|
|
366
374
|
// Look for patterns like core modules that should not depend on peripheral modules
|
|
367
375
|
const coreModules = new Set();
|
|
368
376
|
const peripheralModules = new Set();
|
|
369
|
-
|
|
377
|
+
|
|
370
378
|
for (const edge of edges) {
|
|
371
379
|
// Identify potential architectural layers based on file paths
|
|
372
380
|
if (this.isCoreLayer(edge.from) && this.isPeripheralLayer(edge.to)) {
|
|
@@ -379,7 +387,7 @@ class InvariantDetector {
|
|
|
379
387
|
});
|
|
380
388
|
}
|
|
381
389
|
}
|
|
382
|
-
|
|
390
|
+
|
|
383
391
|
return constraints;
|
|
384
392
|
}
|
|
385
393
|
|
|
@@ -388,19 +396,19 @@ class InvariantDetector {
|
|
|
388
396
|
*/
|
|
389
397
|
async findInvariantRelatedFiles(directory) {
|
|
390
398
|
const invariantFiles = [];
|
|
391
|
-
|
|
399
|
+
|
|
392
400
|
const walk = async (dir) => {
|
|
393
401
|
try {
|
|
394
402
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
395
|
-
|
|
403
|
+
|
|
396
404
|
for (const entry of entries) {
|
|
397
405
|
const fullPath = path.join(dir, entry.name);
|
|
398
|
-
|
|
406
|
+
|
|
399
407
|
if (entry.isDirectory()) {
|
|
400
408
|
await walk(fullPath);
|
|
401
409
|
} else if (entry.isFile()) {
|
|
402
410
|
const fileName = entry.name.toLowerCase();
|
|
403
|
-
|
|
411
|
+
|
|
404
412
|
// Look for files that commonly contain invariants
|
|
405
413
|
if (
|
|
406
414
|
fileName.includes('invariant') ||
|
|
@@ -420,7 +428,7 @@ class InvariantDetector {
|
|
|
420
428
|
// Skip directories we can't access
|
|
421
429
|
}
|
|
422
430
|
};
|
|
423
|
-
|
|
431
|
+
|
|
424
432
|
await walk(directory);
|
|
425
433
|
return invariantFiles;
|
|
426
434
|
}
|
|
@@ -430,7 +438,7 @@ class InvariantDetector {
|
|
|
430
438
|
*/
|
|
431
439
|
searchForInvariantPatterns(content) {
|
|
432
440
|
const patterns = [];
|
|
433
|
-
|
|
441
|
+
|
|
434
442
|
// Look for assertion/verification patterns
|
|
435
443
|
if (content.includes('assert(') || content.includes('AssertionError')) {
|
|
436
444
|
patterns.push({
|
|
@@ -439,7 +447,7 @@ class InvariantDetector {
|
|
|
439
447
|
assertion: 'Code contains assert() calls for invariant enforcement'
|
|
440
448
|
});
|
|
441
449
|
}
|
|
442
|
-
|
|
450
|
+
|
|
443
451
|
// Look for validation patterns
|
|
444
452
|
if (content.includes('validate(') || content.includes('isValid') || content.includes('checkValid')) {
|
|
445
453
|
patterns.push({
|
|
@@ -448,7 +456,7 @@ class InvariantDetector {
|
|
|
448
456
|
assertion: 'Code contains validation functions for invariant enforcement'
|
|
449
457
|
});
|
|
450
458
|
}
|
|
451
|
-
|
|
459
|
+
|
|
452
460
|
// Look for guard clauses
|
|
453
461
|
if (content.includes('if (!condition) throw') || content.includes('guard clause')) {
|
|
454
462
|
patterns.push({
|
|
@@ -457,7 +465,7 @@ class InvariantDetector {
|
|
|
457
465
|
assertion: 'Code contains early-exit guard clauses for invariant enforcement'
|
|
458
466
|
});
|
|
459
467
|
}
|
|
460
|
-
|
|
468
|
+
|
|
461
469
|
// Look for state machine patterns
|
|
462
470
|
if (content.includes('state') && (content.includes('transition') || content.includes('transitions'))) {
|
|
463
471
|
patterns.push({
|
|
@@ -466,7 +474,7 @@ class InvariantDetector {
|
|
|
466
474
|
assertion: 'Code implements state machine with transition constraints'
|
|
467
475
|
});
|
|
468
476
|
}
|
|
469
|
-
|
|
477
|
+
|
|
470
478
|
return patterns;
|
|
471
479
|
}
|
|
472
480
|
|
|
@@ -501,11 +509,11 @@ class InvariantDetector {
|
|
|
501
509
|
*/
|
|
502
510
|
isCoreLayer(filePath) {
|
|
503
511
|
if (!filePath) return false;
|
|
504
|
-
return filePath.includes('/core/') ||
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
return filePath.includes('/core/') ||
|
|
513
|
+
filePath.includes('/lib/') ||
|
|
514
|
+
filePath.includes('/shared/') ||
|
|
515
|
+
filePath.includes('config') ||
|
|
516
|
+
filePath.includes('auth');
|
|
509
517
|
}
|
|
510
518
|
|
|
511
519
|
/**
|
|
@@ -513,41 +521,41 @@ class InvariantDetector {
|
|
|
513
521
|
*/
|
|
514
522
|
isPeripheralLayer(filePath) {
|
|
515
523
|
if (!filePath) return false;
|
|
516
|
-
return filePath.includes('/views/') ||
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
524
|
+
return filePath.includes('/views/') ||
|
|
525
|
+
filePath.includes('/templates/') ||
|
|
526
|
+
filePath.includes('/ui/') ||
|
|
527
|
+
filePath.includes('/tests/') ||
|
|
528
|
+
filePath.includes('/mocks/');
|
|
521
529
|
}
|
|
522
530
|
|
|
523
531
|
/**
|
|
524
532
|
* Generate unique invariant ID
|
|
525
533
|
*/
|
|
526
|
-
generateInvariantId(pattern, context) {
|
|
527
|
-
const baseId = `${pattern}_${context}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
534
|
+
generateInvariantId(pattern, context, discriminator = '') {
|
|
535
|
+
const baseId = `${pattern}_${context}_${discriminator}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
528
536
|
return baseId.substring(0, 100); // Limit length
|
|
529
537
|
}
|
|
530
|
-
|
|
538
|
+
|
|
531
539
|
/**
|
|
532
540
|
* Calculate confidence score for an invariant based on various factors
|
|
533
541
|
*/
|
|
534
542
|
calculateConfidenceScore(pattern, context, content = '') {
|
|
535
543
|
let score = 0.5; // Base confidence
|
|
536
|
-
|
|
544
|
+
|
|
537
545
|
// Increase confidence based on pattern type
|
|
538
546
|
if (pattern.includes('assert') || pattern.includes('validate') || pattern.includes('guard')) {
|
|
539
547
|
score += 0.2;
|
|
540
548
|
}
|
|
541
|
-
|
|
549
|
+
|
|
542
550
|
if (pattern.includes('state') || pattern.includes('mutation') || pattern.includes('setState')) {
|
|
543
551
|
score += 0.15;
|
|
544
552
|
}
|
|
545
|
-
|
|
553
|
+
|
|
546
554
|
// Increase confidence if context suggests critical file
|
|
547
555
|
if (context.includes('core') || context.includes('lib') || context.includes('shared')) {
|
|
548
556
|
score += 0.1;
|
|
549
557
|
}
|
|
550
|
-
|
|
558
|
+
|
|
551
559
|
// Increase confidence based on frequency of pattern in content
|
|
552
560
|
if (content && typeof content === 'string') {
|
|
553
561
|
const matches = content.match(new RegExp(pattern, 'gi'));
|
|
@@ -555,50 +563,50 @@ class InvariantDetector {
|
|
|
555
563
|
score += Math.min(matches.length * 0.05, 0.2); // Up to 0.2 for multiple matches
|
|
556
564
|
}
|
|
557
565
|
}
|
|
558
|
-
|
|
566
|
+
|
|
559
567
|
// Ensure score stays within bounds
|
|
560
568
|
return Math.min(Math.max(score, 0.1), 1.0);
|
|
561
569
|
}
|
|
562
|
-
|
|
570
|
+
|
|
563
571
|
/**
|
|
564
572
|
* Determine the owner of an invariant based on the source file
|
|
565
573
|
*/
|
|
566
574
|
determineOwnerFromPath(filePath) {
|
|
567
575
|
if (!filePath) return 'unknown';
|
|
568
|
-
|
|
576
|
+
|
|
569
577
|
// Extract function or module name from file path
|
|
570
578
|
const parts = filePath.split(/[\/]/);
|
|
571
579
|
const fileName = parts[parts.length - 1];
|
|
572
580
|
const baseName = fileName.replace(/\.[^/.]+$/, ''); // Remove extension
|
|
573
|
-
|
|
581
|
+
|
|
574
582
|
return baseName;
|
|
575
583
|
}
|
|
576
|
-
|
|
584
|
+
|
|
577
585
|
/**
|
|
578
586
|
* Analyze structural patterns in the codebase and generate architectural invariants
|
|
579
587
|
*/
|
|
580
588
|
async analyzeStructuralPatterns(nodes, edges, directory) {
|
|
581
589
|
// Generate layer-based architectural invariants
|
|
582
590
|
await this.generateLayerInvariants(nodes, edges);
|
|
583
|
-
|
|
591
|
+
|
|
584
592
|
// Generate dependency-based architectural invariants
|
|
585
593
|
await this.generateDependencyInvariants(nodes, edges);
|
|
586
|
-
|
|
594
|
+
|
|
587
595
|
// Generate coupling-based architectural invariants
|
|
588
596
|
await this.generateCouplingInvariants(nodes, edges);
|
|
589
597
|
}
|
|
590
|
-
|
|
598
|
+
|
|
591
599
|
/**
|
|
592
600
|
* Generate layer-based architectural invariants
|
|
593
601
|
*/
|
|
594
602
|
async generateLayerInvariants(nodes, edges) {
|
|
595
603
|
const layerMap = new Map();
|
|
596
|
-
|
|
604
|
+
|
|
597
605
|
// Classify nodes by layer based on path patterns
|
|
598
606
|
for (const node of nodes) {
|
|
599
607
|
const filePath = node.path || node.id;
|
|
600
608
|
if (!filePath) continue;
|
|
601
|
-
|
|
609
|
+
|
|
602
610
|
let layer = 'general';
|
|
603
611
|
if (filePath.includes('/controllers/') || filePath.includes('/handlers/')) {
|
|
604
612
|
layer = 'controllers';
|
|
@@ -613,31 +621,31 @@ class InvariantDetector {
|
|
|
613
621
|
} else if (filePath.includes('/views/') || filePath.includes('/components/') || filePath.includes('/ui/')) {
|
|
614
622
|
layer = 'presentation';
|
|
615
623
|
}
|
|
616
|
-
|
|
624
|
+
|
|
617
625
|
if (!layerMap.has(layer)) {
|
|
618
626
|
layerMap.set(layer, []);
|
|
619
627
|
}
|
|
620
628
|
layerMap.get(layer).push(filePath);
|
|
621
629
|
}
|
|
622
|
-
|
|
630
|
+
|
|
623
631
|
// Generate invariants based on layer dependencies (e.g., models shouldn't depend on controllers)
|
|
624
632
|
const layerDependencies = new Map();
|
|
625
633
|
for (const edge of edges) {
|
|
626
634
|
const fromNode = nodes.find(n => n.id === edge.from || n.path === edge.from);
|
|
627
635
|
const toNode = nodes.find(n => n.id === edge.to || n.path === edge.to);
|
|
628
|
-
|
|
636
|
+
|
|
629
637
|
if (fromNode && toNode) {
|
|
630
638
|
const fromPath = fromNode.path || fromNode.id;
|
|
631
639
|
const toPath = toNode.path || toNode.id;
|
|
632
|
-
|
|
640
|
+
|
|
633
641
|
let fromLayer = 'general';
|
|
634
642
|
let toLayer = 'general';
|
|
635
|
-
|
|
643
|
+
|
|
636
644
|
for (const [layer, paths] of layerMap.entries()) {
|
|
637
645
|
if (paths.includes(fromPath)) fromLayer = layer;
|
|
638
646
|
if (paths.includes(toPath)) toLayer = layer;
|
|
639
647
|
}
|
|
640
|
-
|
|
648
|
+
|
|
641
649
|
if (fromLayer !== toLayer) {
|
|
642
650
|
const depKey = `${fromLayer}->${toLayer}`;
|
|
643
651
|
if (!layerDependencies.has(depKey)) {
|
|
@@ -647,23 +655,23 @@ class InvariantDetector {
|
|
|
647
655
|
}
|
|
648
656
|
}
|
|
649
657
|
}
|
|
650
|
-
|
|
658
|
+
|
|
651
659
|
// Create invariants for problematic layer dependencies
|
|
652
660
|
for (const [depKey, dependencies] of layerDependencies.entries()) {
|
|
653
661
|
const [fromLayer, toLayer] = depKey.split('->');
|
|
654
|
-
|
|
662
|
+
|
|
655
663
|
// Define architectural rules (e.g., models should not depend on controllers)
|
|
656
664
|
const forbiddenLayerDeps = [
|
|
657
665
|
{ from: 'models', to: 'controllers' },
|
|
658
|
-
{ from: 'models', to: 'services' },
|
|
666
|
+
{ from: 'models', to: 'services' },
|
|
659
667
|
{ from: 'controllers', to: 'models' },
|
|
660
668
|
{ from: 'presentation', to: 'models' }
|
|
661
669
|
];
|
|
662
|
-
|
|
663
|
-
const isForbidden = forbiddenLayerDeps.some(rule =>
|
|
670
|
+
|
|
671
|
+
const isForbidden = forbiddenLayerDeps.some(rule =>
|
|
664
672
|
rule.from === fromLayer && rule.to === toLayer
|
|
665
673
|
);
|
|
666
|
-
|
|
674
|
+
|
|
667
675
|
if (isForbidden) {
|
|
668
676
|
this.detectedInvariants.push({
|
|
669
677
|
id: this.generateInvariantId(`layer_${fromLayer}_not_depend_${toLayer}`, `${fromLayer}_${toLayer}`),
|
|
@@ -692,14 +700,14 @@ class InvariantDetector {
|
|
|
692
700
|
}
|
|
693
701
|
}
|
|
694
702
|
}
|
|
695
|
-
|
|
703
|
+
|
|
696
704
|
/**
|
|
697
705
|
* Generate dependency-based architectural invariants
|
|
698
706
|
*/
|
|
699
707
|
async generateDependencyInvariants(nodes, edges) {
|
|
700
708
|
// Identify high-degree nodes that might indicate architectural hubs
|
|
701
709
|
const nodeDegrees = new Map();
|
|
702
|
-
|
|
710
|
+
|
|
703
711
|
for (const edge of edges) {
|
|
704
712
|
// Count incoming edges (dependencies on this node)
|
|
705
713
|
if (!nodeDegrees.has(edge.to)) {
|
|
@@ -708,11 +716,11 @@ class InvariantDetector {
|
|
|
708
716
|
if (!nodeDegrees.has(edge.from)) {
|
|
709
717
|
nodeDegrees.set(edge.from, { incoming: 0, outgoing: 0 });
|
|
710
718
|
}
|
|
711
|
-
|
|
719
|
+
|
|
712
720
|
nodeDegrees.get(edge.to).incoming++;
|
|
713
721
|
nodeDegrees.get(edge.from).outgoing++;
|
|
714
722
|
}
|
|
715
|
-
|
|
723
|
+
|
|
716
724
|
// Identify potential architectural bottlenecks
|
|
717
725
|
for (const [nodeId, degrees] of nodeDegrees.entries()) {
|
|
718
726
|
// Make sure nodeId is valid before creating invariant
|
|
@@ -743,18 +751,18 @@ class InvariantDetector {
|
|
|
743
751
|
}
|
|
744
752
|
}
|
|
745
753
|
}
|
|
746
|
-
|
|
754
|
+
|
|
747
755
|
/**
|
|
748
756
|
* Generate coupling-based architectural invariants
|
|
749
757
|
*/
|
|
750
758
|
async generateCouplingInvariants(nodes, edges) {
|
|
751
759
|
// Identify tightly coupled components
|
|
752
760
|
const couplingMap = new Map();
|
|
753
|
-
|
|
761
|
+
|
|
754
762
|
for (const edge of edges) {
|
|
755
763
|
// Look for mutual dependencies indicating tight coupling
|
|
756
764
|
const reverseEdge = edges.find(e => e.from === edge.to && e.to === edge.from);
|
|
757
|
-
|
|
765
|
+
|
|
758
766
|
if (reverseEdge) {
|
|
759
767
|
const coupleKey = [edge.from, edge.to].sort().join('|');
|
|
760
768
|
if (!couplingMap.has(coupleKey)) {
|
|
@@ -763,11 +771,11 @@ class InvariantDetector {
|
|
|
763
771
|
couplingMap.get(coupleKey).push(edge);
|
|
764
772
|
}
|
|
765
773
|
}
|
|
766
|
-
|
|
774
|
+
|
|
767
775
|
// Create invariants for tightly coupled components
|
|
768
776
|
for (const [coupleKey, edgesList] of couplingMap.entries()) {
|
|
769
777
|
const [nodeA, nodeB] = coupleKey.split('|');
|
|
770
|
-
|
|
778
|
+
|
|
771
779
|
// Only create the invariant if both node names are valid and non-empty
|
|
772
780
|
if (nodeA && nodeA.trim() !== '' && nodeB && nodeB.trim() !== '') {
|
|
773
781
|
this.detectedInvariants.push({
|
|
@@ -794,6 +802,18 @@ class InvariantDetector {
|
|
|
794
802
|
}
|
|
795
803
|
}
|
|
796
804
|
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Check if a file is accessible and readable
|
|
808
|
+
*/
|
|
809
|
+
async isFileAccessible(filePath) {
|
|
810
|
+
try {
|
|
811
|
+
await fs.access(filePath, require('fs').constants.R_OK);
|
|
812
|
+
return true;
|
|
813
|
+
} catch (error) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
797
817
|
}
|
|
798
818
|
|
|
799
819
|
module.exports = { InvariantDetector };
|