thuban 0.3.2 → 0.3.3
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 +137 -102
- package/cli.js +310 -95
- package/package.json +5 -2
- package/packages/scanner/alert-manager.js +398 -398
- package/packages/scanner/code-scanner.js +758 -713
- package/packages/scanner/copy-paste-detector.js +33 -7
- package/packages/scanner/dependency-graph.js +541 -536
- package/packages/scanner/drift-detector.js +423 -423
- package/packages/scanner/file-collector.js +64 -0
- package/packages/scanner/file-watcher.js +323 -323
- package/packages/scanner/ghost-code-detector.js +80 -5
- package/packages/scanner/hallucination-detector.js +189 -19
- package/packages/scanner/health-checker.js +586 -586
- package/packages/scanner/index.js +100 -92
- package/packages/scanner/license-manager.js +15 -2
- package/packages/scanner/master-health-checker.js +513 -513
- package/packages/scanner/monitor-notifier.js +64 -0
- package/packages/scanner/monitor-service.js +103 -0
- package/packages/scanner/monitor-store.js +117 -0
- package/packages/scanner/pre-commit-gate.js +1 -1
- package/packages/scanner/scan-diff.js +50 -0
- package/packages/scanner/scan-runner.js +172 -0
- package/packages/scanner/secret-scanner.js +612 -0
- package/packages/scanner/secret-scanner.test.js +103 -0
- package/packages/scanner/sentinel-core.js +616 -608
- package/packages/scanner/sentinel-knowledge.js +322 -322
- package/packages/scanner/tech-debt-analyzer.js +46 -28
- package/packages/scanner/widget-generator.js +415 -415
|
@@ -1,536 +1,541 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sentinel Dependency Graph
|
|
3
|
-
*
|
|
4
|
-
* Maps all file dependencies in the Orion codebase.
|
|
5
|
-
* Answers: "If I change X, what breaks?"
|
|
6
|
-
*
|
|
7
|
-
* @purpose Track and analyze file dependencies across codebase
|
|
8
|
-
* @module sentinel
|
|
9
|
-
* @layer infrastructure
|
|
10
|
-
* @exports-to sentinel-core, widget-generator
|
|
11
|
-
* @side-effects File system reads
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const EventEmitter = require('events');
|
|
17
|
-
|
|
18
|
-
class DependencyGraph extends EventEmitter {
|
|
19
|
-
constructor(config = {}) {
|
|
20
|
-
super();
|
|
21
|
-
|
|
22
|
-
this.config = {
|
|
23
|
-
rootPath: config.rootPath || process.cwd(),
|
|
24
|
-
ignorePatterns: config.ignorePatterns || [
|
|
25
|
-
'node_modules', '.git', 'dist', '.vite', '.orion/snapshots'
|
|
26
|
-
],
|
|
27
|
-
extensions: config.extensions || ['.js', '.ts', '.jsx', '.tsx'],
|
|
28
|
-
...config
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// The graph: file -> { imports: [], importedBy: [] }
|
|
32
|
-
this.graph = new Map();
|
|
33
|
-
|
|
34
|
-
// Module resolution cache
|
|
35
|
-
this.resolveCache = new Map();
|
|
36
|
-
|
|
37
|
-
// Statistics
|
|
38
|
-
this.stats = {
|
|
39
|
-
filesAnalyzed: 0,
|
|
40
|
-
totalImports: 0,
|
|
41
|
-
unresolvedImports: 0,
|
|
42
|
-
circularDependencies: []
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel Dependency Graph
|
|
3
|
+
*
|
|
4
|
+
* Maps all file dependencies in the Orion codebase.
|
|
5
|
+
* Answers: "If I change X, what breaks?"
|
|
6
|
+
*
|
|
7
|
+
* @purpose Track and analyze file dependencies across codebase
|
|
8
|
+
* @module sentinel
|
|
9
|
+
* @layer infrastructure
|
|
10
|
+
* @exports-to sentinel-core, widget-generator
|
|
11
|
+
* @side-effects File system reads
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const EventEmitter = require('events');
|
|
17
|
+
|
|
18
|
+
class DependencyGraph extends EventEmitter {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
super();
|
|
21
|
+
|
|
22
|
+
this.config = {
|
|
23
|
+
rootPath: config.rootPath || process.cwd(),
|
|
24
|
+
ignorePatterns: config.ignorePatterns || [
|
|
25
|
+
'node_modules', '.git', 'dist', '.vite', '.orion/snapshots'
|
|
26
|
+
],
|
|
27
|
+
extensions: config.extensions || ['.js', '.ts', '.jsx', '.tsx', '.php', '.rb'],
|
|
28
|
+
...config
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// The graph: file -> { imports: [], importedBy: [] }
|
|
32
|
+
this.graph = new Map();
|
|
33
|
+
|
|
34
|
+
// Module resolution cache
|
|
35
|
+
this.resolveCache = new Map();
|
|
36
|
+
|
|
37
|
+
// Statistics
|
|
38
|
+
this.stats = {
|
|
39
|
+
filesAnalyzed: 0,
|
|
40
|
+
totalImports: 0,
|
|
41
|
+
unresolvedImports: 0,
|
|
42
|
+
circularDependencies: []
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Convenience getter so callers can use graph.circularDeps || []
|
|
47
|
+
get circularDeps() {
|
|
48
|
+
return this.stats.circularDependencies;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the full dependency graph
|
|
53
|
+
*/
|
|
54
|
+
async build() {
|
|
55
|
+
// debug logging silenced for clean output
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
|
|
58
|
+
// Clear existing graph
|
|
59
|
+
this.graph.clear();
|
|
60
|
+
this.resolveCache.clear();
|
|
61
|
+
this.stats = {
|
|
62
|
+
filesAnalyzed: 0,
|
|
63
|
+
totalImports: 0,
|
|
64
|
+
unresolvedImports: 0,
|
|
65
|
+
circularDependencies: []
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Discover all files
|
|
69
|
+
const files = await this._discoverFiles(this.config.rootPath);
|
|
70
|
+
|
|
71
|
+
// First pass: extract all imports
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
await this._analyzeFile(file);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Second pass: build reverse dependencies (importedBy)
|
|
77
|
+
this._buildReverseDependencies();
|
|
78
|
+
|
|
79
|
+
// Third pass: detect circular dependencies
|
|
80
|
+
this._detectCircularDependencies();
|
|
81
|
+
|
|
82
|
+
const duration = Date.now() - startTime;
|
|
83
|
+
|
|
84
|
+
return this.getStats();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get what a file depends on (imports)
|
|
89
|
+
*/
|
|
90
|
+
getDependencies(filePath) {
|
|
91
|
+
const normalized = this._normalizePath(filePath);
|
|
92
|
+
const node = this.graph.get(normalized);
|
|
93
|
+
return node ? node.imports : [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get what depends on a file (dependents)
|
|
98
|
+
*/
|
|
99
|
+
getDependents(filePath) {
|
|
100
|
+
const normalized = this._normalizePath(filePath);
|
|
101
|
+
const node = this.graph.get(normalized);
|
|
102
|
+
return node ? node.importedBy : [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get full dependency chain (recursive)
|
|
107
|
+
*/
|
|
108
|
+
getFullDependencyChain(filePath, visited = new Set()) {
|
|
109
|
+
const normalized = this._normalizePath(filePath);
|
|
110
|
+
if (visited.has(normalized)) return []; // Prevent infinite loop
|
|
111
|
+
|
|
112
|
+
visited.add(normalized);
|
|
113
|
+
const deps = this.getDependencies(normalized);
|
|
114
|
+
const chain = [...deps];
|
|
115
|
+
|
|
116
|
+
for (const dep of deps) {
|
|
117
|
+
if (dep.resolved) {
|
|
118
|
+
chain.push(...this.getFullDependencyChain(dep.resolved, visited));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return chain;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* IMPACT ANALYSIS: What could break if I change this file?
|
|
127
|
+
*/
|
|
128
|
+
getImpactAnalysis(filePath) {
|
|
129
|
+
const normalized = this._normalizePath(filePath);
|
|
130
|
+
const result = {
|
|
131
|
+
file: normalized,
|
|
132
|
+
relativePath: path.relative(this.config.rootPath, normalized),
|
|
133
|
+
directDependents: [],
|
|
134
|
+
indirectDependents: [],
|
|
135
|
+
totalImpact: 0,
|
|
136
|
+
riskLevel: 'low',
|
|
137
|
+
affectedModules: new Set()
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Get direct dependents
|
|
141
|
+
result.directDependents = this.getDependents(normalized).map(d => ({
|
|
142
|
+
file: d,
|
|
143
|
+
relativePath: path.relative(this.config.rootPath, d)
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
// Get indirect dependents (recursive)
|
|
147
|
+
const visited = new Set([normalized]);
|
|
148
|
+
const queue = [...result.directDependents.map(d => d.file)];
|
|
149
|
+
|
|
150
|
+
while (queue.length > 0) {
|
|
151
|
+
const current = queue.shift();
|
|
152
|
+
if (visited.has(current)) continue;
|
|
153
|
+
visited.add(current);
|
|
154
|
+
|
|
155
|
+
const dependents = this.getDependents(current);
|
|
156
|
+
for (const dep of dependents) {
|
|
157
|
+
if (!visited.has(dep)) {
|
|
158
|
+
result.indirectDependents.push({
|
|
159
|
+
file: dep,
|
|
160
|
+
relativePath: path.relative(this.config.rootPath, dep),
|
|
161
|
+
via: path.relative(this.config.rootPath, current)
|
|
162
|
+
});
|
|
163
|
+
queue.push(dep);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Calculate total impact
|
|
169
|
+
result.totalImpact = result.directDependents.length + result.indirectDependents.length;
|
|
170
|
+
|
|
171
|
+
// Determine risk level
|
|
172
|
+
if (result.totalImpact === 0) {
|
|
173
|
+
result.riskLevel = 'none';
|
|
174
|
+
} else if (result.totalImpact <= 3) {
|
|
175
|
+
result.riskLevel = 'low';
|
|
176
|
+
} else if (result.totalImpact <= 10) {
|
|
177
|
+
result.riskLevel = 'medium';
|
|
178
|
+
} else if (result.totalImpact <= 25) {
|
|
179
|
+
result.riskLevel = 'high';
|
|
180
|
+
} else {
|
|
181
|
+
result.riskLevel = 'critical';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Extract affected modules
|
|
185
|
+
for (const dep of [...result.directDependents, ...result.indirectDependents]) {
|
|
186
|
+
const parts = dep.relativePath.split(path.sep);
|
|
187
|
+
if (parts.length > 0) {
|
|
188
|
+
result.affectedModules.add(parts[0]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
result.affectedModules = Array.from(result.affectedModules);
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Find most critical files (most dependents)
|
|
198
|
+
*/
|
|
199
|
+
getMostCriticalFiles(limit = 20) {
|
|
200
|
+
const files = [];
|
|
201
|
+
|
|
202
|
+
for (const [filePath, node] of this.graph) {
|
|
203
|
+
files.push({
|
|
204
|
+
file: filePath,
|
|
205
|
+
relativePath: path.relative(this.config.rootPath, filePath),
|
|
206
|
+
dependentCount: node.importedBy.length,
|
|
207
|
+
dependencyCount: node.imports.length
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return files
|
|
212
|
+
.sort((a, b) => b.dependentCount - a.dependentCount)
|
|
213
|
+
.slice(0, limit);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Find orphan files (no imports, no dependents)
|
|
218
|
+
*/
|
|
219
|
+
getOrphanFiles() {
|
|
220
|
+
const orphans = [];
|
|
221
|
+
|
|
222
|
+
for (const [filePath, node] of this.graph) {
|
|
223
|
+
if (node.imports.length === 0 && node.importedBy.length === 0) {
|
|
224
|
+
orphans.push({
|
|
225
|
+
file: filePath,
|
|
226
|
+
relativePath: path.relative(this.config.rootPath, filePath)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return orphans;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get module dependency summary
|
|
236
|
+
*/
|
|
237
|
+
getModuleSummary() {
|
|
238
|
+
const modules = new Map();
|
|
239
|
+
|
|
240
|
+
for (const [filePath, node] of this.graph) {
|
|
241
|
+
const relativePath = path.relative(this.config.rootPath, filePath);
|
|
242
|
+
const parts = relativePath.split(path.sep);
|
|
243
|
+
const moduleName = parts[0];
|
|
244
|
+
|
|
245
|
+
if (!modules.has(moduleName)) {
|
|
246
|
+
modules.set(moduleName, {
|
|
247
|
+
name: moduleName,
|
|
248
|
+
files: [],
|
|
249
|
+
internalDeps: 0,
|
|
250
|
+
externalDeps: 0,
|
|
251
|
+
dependedOnBy: new Set()
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const mod = modules.get(moduleName);
|
|
256
|
+
mod.files.push(relativePath);
|
|
257
|
+
|
|
258
|
+
// Count internal vs external dependencies
|
|
259
|
+
for (const imp of node.imports) {
|
|
260
|
+
if (imp.resolved) {
|
|
261
|
+
const impRelative = path.relative(this.config.rootPath, imp.resolved);
|
|
262
|
+
const impModule = impRelative.split(path.sep)[0];
|
|
263
|
+
if (impModule === moduleName) {
|
|
264
|
+
mod.internalDeps++;
|
|
265
|
+
} else {
|
|
266
|
+
mod.externalDeps++;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Track which modules depend on this one
|
|
272
|
+
for (const depPath of node.importedBy) {
|
|
273
|
+
const depRelative = path.relative(this.config.rootPath, depPath);
|
|
274
|
+
const depModule = depRelative.split(path.sep)[0];
|
|
275
|
+
if (depModule !== moduleName) {
|
|
276
|
+
mod.dependedOnBy.add(depModule);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Convert to array and calculate metrics
|
|
282
|
+
const result = [];
|
|
283
|
+
for (const [name, mod] of modules) {
|
|
284
|
+
result.push({
|
|
285
|
+
name,
|
|
286
|
+
fileCount: mod.files.length,
|
|
287
|
+
internalDeps: mod.internalDeps,
|
|
288
|
+
externalDeps: mod.externalDeps,
|
|
289
|
+
dependedOnByCount: mod.dependedOnBy.size,
|
|
290
|
+
dependedOnBy: Array.from(mod.dependedOnBy)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result.sort((a, b) => b.fileCount - a.fileCount);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get statistics
|
|
299
|
+
*/
|
|
300
|
+
getStats() {
|
|
301
|
+
return {
|
|
302
|
+
...this.stats,
|
|
303
|
+
totalFiles: this.graph.size,
|
|
304
|
+
circularCount: this.stats.circularDependencies.length
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Export graph as JSON
|
|
310
|
+
*/
|
|
311
|
+
toJSON() {
|
|
312
|
+
const data = {
|
|
313
|
+
generated: new Date().toISOString(),
|
|
314
|
+
rootPath: this.config.rootPath,
|
|
315
|
+
stats: this.getStats(),
|
|
316
|
+
files: {}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
for (const [filePath, node] of this.graph) {
|
|
320
|
+
const relativePath = path.relative(this.config.rootPath, filePath);
|
|
321
|
+
data.files[relativePath] = {
|
|
322
|
+
imports: node.imports.map(i => ({
|
|
323
|
+
module: i.module,
|
|
324
|
+
resolved: i.resolved ? path.relative(this.config.rootPath, i.resolved) : null,
|
|
325
|
+
specifiers: i.specifiers
|
|
326
|
+
})),
|
|
327
|
+
importedBy: node.importedBy.map(p => path.relative(this.config.rootPath, p))
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return data;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Private methods
|
|
335
|
+
|
|
336
|
+
async _discoverFiles(dir, files = []) {
|
|
337
|
+
try {
|
|
338
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
339
|
+
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
const fullPath = path.join(dir, entry.name);
|
|
342
|
+
|
|
343
|
+
// Check ignore patterns
|
|
344
|
+
if (this._shouldIgnore(entry.name)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (entry.isDirectory()) {
|
|
349
|
+
await this._discoverFiles(fullPath, files);
|
|
350
|
+
} else if (entry.isFile()) {
|
|
351
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
352
|
+
if (this.config.extensions.includes(ext)) {
|
|
353
|
+
files.push(fullPath);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
// Skip inaccessible directories
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return files;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_shouldIgnore(name) {
|
|
365
|
+
return this.config.ignorePatterns.some(pattern =>
|
|
366
|
+
name === pattern || name.startsWith(pattern)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async _analyzeFile(filePath) {
|
|
371
|
+
try {
|
|
372
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
373
|
+
const imports = this._extractImports(content, filePath);
|
|
374
|
+
|
|
375
|
+
this.graph.set(filePath, {
|
|
376
|
+
imports,
|
|
377
|
+
importedBy: [] // Will be filled in second pass
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
this.stats.filesAnalyzed++;
|
|
381
|
+
this.stats.totalImports += imports.length;
|
|
382
|
+
this.stats.unresolvedImports += imports.filter(i => !i.resolved).length;
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
// Skip files that can't be read
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_extractImports(content, filePath) {
|
|
390
|
+
const imports = [];
|
|
391
|
+
const fileDir = path.dirname(filePath);
|
|
392
|
+
|
|
393
|
+
// CommonJS: require('module') or require("module")
|
|
394
|
+
const requireRegex = /(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
395
|
+
let match;
|
|
396
|
+
|
|
397
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
398
|
+
const specifiers = match[1]
|
|
399
|
+
? match[1].split(',').map(s => s.trim().split(':')[0].trim()).filter(Boolean)
|
|
400
|
+
: [match[2]];
|
|
401
|
+
const modulePath = match[3];
|
|
402
|
+
|
|
403
|
+
imports.push({
|
|
404
|
+
module: modulePath,
|
|
405
|
+
specifiers,
|
|
406
|
+
resolved: this._resolveModule(modulePath, fileDir),
|
|
407
|
+
type: 'require'
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Also catch: require('module') without assignment
|
|
412
|
+
const bareRequireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
413
|
+
while ((match = bareRequireRegex.exec(content)) !== null) {
|
|
414
|
+
const modulePath = match[1];
|
|
415
|
+
// Skip if already captured
|
|
416
|
+
if (!imports.some(i => i.module === modulePath)) {
|
|
417
|
+
imports.push({
|
|
418
|
+
module: modulePath,
|
|
419
|
+
specifiers: [],
|
|
420
|
+
resolved: this._resolveModule(modulePath, fileDir),
|
|
421
|
+
type: 'require'
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ES6: import ... from 'module'
|
|
427
|
+
const importRegex = /import\s+(?:(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*)?)+\s+from\s+['"]([^'"]+)['"]/g;
|
|
428
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
429
|
+
const specifiers = [];
|
|
430
|
+
if (match[1]) specifiers.push(...match[1].split(',').map(s => s.trim().split(' as ')[0].trim()).filter(Boolean));
|
|
431
|
+
if (match[2]) specifiers.push(match[2]);
|
|
432
|
+
if (match[3]) specifiers.push(match[3]);
|
|
433
|
+
const modulePath = match[4];
|
|
434
|
+
|
|
435
|
+
imports.push({
|
|
436
|
+
module: modulePath,
|
|
437
|
+
specifiers,
|
|
438
|
+
resolved: this._resolveModule(modulePath, fileDir),
|
|
439
|
+
type: 'import'
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return imports;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_resolveModule(modulePath, fromDir) {
|
|
447
|
+
// Check cache
|
|
448
|
+
const cacheKey = `${fromDir}:${modulePath}`;
|
|
449
|
+
if (this.resolveCache.has(cacheKey)) {
|
|
450
|
+
return this.resolveCache.get(cacheKey);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let resolved = null;
|
|
454
|
+
|
|
455
|
+
// Skip external modules (node_modules)
|
|
456
|
+
if (!modulePath.startsWith('.') && !modulePath.startsWith('/')) {
|
|
457
|
+
this.resolveCache.set(cacheKey, null);
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Try to resolve relative path
|
|
462
|
+
const basePath = path.resolve(fromDir, modulePath);
|
|
463
|
+
const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '.php', '.rb', '/index.js', '/index.ts'];
|
|
464
|
+
|
|
465
|
+
for (const ext of extensions) {
|
|
466
|
+
const tryPath = basePath + ext;
|
|
467
|
+
if (fs.existsSync(tryPath) && fs.statSync(tryPath).isFile()) {
|
|
468
|
+
resolved = tryPath;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
this.resolveCache.set(cacheKey, resolved);
|
|
474
|
+
return resolved;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_buildReverseDependencies() {
|
|
478
|
+
for (const [filePath, node] of this.graph) {
|
|
479
|
+
for (const imp of node.imports) {
|
|
480
|
+
if (imp.resolved && this.graph.has(imp.resolved)) {
|
|
481
|
+
const targetNode = this.graph.get(imp.resolved);
|
|
482
|
+
if (!targetNode.importedBy.includes(filePath)) {
|
|
483
|
+
targetNode.importedBy.push(filePath);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_detectCircularDependencies() {
|
|
491
|
+
const visited = new Set();
|
|
492
|
+
const recursionStack = new Set();
|
|
493
|
+
|
|
494
|
+
const dfs = (filePath, chain = []) => {
|
|
495
|
+
if (recursionStack.has(filePath)) {
|
|
496
|
+
// Found circular dependency
|
|
497
|
+
const cycleStart = chain.indexOf(filePath);
|
|
498
|
+
const cycle = chain.slice(cycleStart).map(p =>
|
|
499
|
+
path.relative(this.config.rootPath, p)
|
|
500
|
+
);
|
|
501
|
+
cycle.push(path.relative(this.config.rootPath, filePath));
|
|
502
|
+
|
|
503
|
+
// Only add if not already recorded
|
|
504
|
+
const cycleKey = cycle.sort().join(' -> ');
|
|
505
|
+
if (!this.stats.circularDependencies.some(c => c.sort().join(' -> ') === cycleKey)) {
|
|
506
|
+
this.stats.circularDependencies.push(cycle);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (visited.has(filePath)) return;
|
|
512
|
+
|
|
513
|
+
visited.add(filePath);
|
|
514
|
+
recursionStack.add(filePath);
|
|
515
|
+
|
|
516
|
+
const node = this.graph.get(filePath);
|
|
517
|
+
if (node) {
|
|
518
|
+
for (const imp of node.imports) {
|
|
519
|
+
if (imp.resolved) {
|
|
520
|
+
dfs(imp.resolved, [...chain, filePath]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
recursionStack.delete(filePath);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
for (const filePath of this.graph.keys()) {
|
|
529
|
+
dfs(filePath);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
_normalizePath(filePath) {
|
|
534
|
+
if (path.isAbsolute(filePath)) {
|
|
535
|
+
return filePath;
|
|
536
|
+
}
|
|
537
|
+
return path.resolve(this.config.rootPath, filePath);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
module.exports = DependencyGraph;
|