@aiready/context-analyzer 0.21.5 → 0.21.7

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.
Files changed (163) hide show
  1. package/.aiready/aiready-report-20260314-222254.json +39216 -0
  2. package/.aiready/aiready-report-20260314-223947.json +3413 -0
  3. package/.aiready/aiready-report-20260314-224112.json +3413 -0
  4. package/.aiready/aiready-report-20260314-224302.json +2973 -0
  5. package/.aiready/aiready-report-20260314-224939.json +3092 -0
  6. package/.aiready/aiready-report-20260314-225154.json +3092 -0
  7. package/.turbo/turbo-build.log +26 -24
  8. package/.turbo/turbo-lint.log +5 -6
  9. package/.turbo/turbo-test.log +41 -119
  10. package/dist/__tests__/analyzer.test.js +55 -14
  11. package/dist/__tests__/analyzer.test.js.map +1 -1
  12. package/dist/__tests__/cluster-detector.test.d.ts +2 -0
  13. package/dist/__tests__/cluster-detector.test.d.ts.map +1 -0
  14. package/dist/__tests__/cluster-detector.test.js +121 -0
  15. package/dist/__tests__/cluster-detector.test.js.map +1 -0
  16. package/dist/__tests__/contract.test.d.ts +2 -0
  17. package/dist/__tests__/contract.test.d.ts.map +1 -0
  18. package/dist/__tests__/contract.test.js +59 -0
  19. package/dist/__tests__/contract.test.js.map +1 -0
  20. package/dist/__tests__/enhanced-cohesion.test.js +12 -2
  21. package/dist/__tests__/enhanced-cohesion.test.js.map +1 -1
  22. package/dist/__tests__/file-classification.test.d.ts +2 -0
  23. package/dist/__tests__/file-classification.test.d.ts.map +1 -0
  24. package/dist/__tests__/file-classification.test.js +749 -0
  25. package/dist/__tests__/file-classification.test.js.map +1 -0
  26. package/dist/__tests__/fragmentation-advanced.test.js +2 -8
  27. package/dist/__tests__/fragmentation-advanced.test.js.map +1 -1
  28. package/dist/__tests__/fragmentation-coupling.test.js +2 -2
  29. package/dist/__tests__/fragmentation-coupling.test.js.map +1 -1
  30. package/dist/__tests__/fragmentation-log.test.js +3 -7
  31. package/dist/__tests__/fragmentation-log.test.js.map +1 -1
  32. package/dist/__tests__/provider.test.d.ts +2 -0
  33. package/dist/__tests__/provider.test.d.ts.map +1 -0
  34. package/dist/__tests__/provider.test.js +72 -0
  35. package/dist/__tests__/provider.test.js.map +1 -0
  36. package/dist/__tests__/remediation.test.d.ts +2 -0
  37. package/dist/__tests__/remediation.test.d.ts.map +1 -0
  38. package/dist/__tests__/remediation.test.js +61 -0
  39. package/dist/__tests__/remediation.test.js.map +1 -0
  40. package/dist/__tests__/scoring.test.js +196 -16
  41. package/dist/__tests__/scoring.test.js.map +1 -1
  42. package/dist/__tests__/structural-cohesion.test.js +8 -2
  43. package/dist/__tests__/structural-cohesion.test.js.map +1 -1
  44. package/dist/analyzer.d.ts +31 -94
  45. package/dist/analyzer.d.ts.map +1 -1
  46. package/dist/analyzer.js +260 -678
  47. package/dist/analyzer.js.map +1 -1
  48. package/dist/analyzers/python-context.d.ts.map +1 -1
  49. package/dist/analyzers/python-context.js +10 -8
  50. package/dist/analyzers/python-context.js.map +1 -1
  51. package/dist/ast-utils.d.ts +16 -0
  52. package/dist/ast-utils.d.ts.map +1 -0
  53. package/dist/ast-utils.js +81 -0
  54. package/dist/ast-utils.js.map +1 -0
  55. package/dist/chunk-2HE27YEV.mjs +1739 -0
  56. package/dist/chunk-64U3PNO3.mjs +94 -0
  57. package/dist/chunk-CDIVYADN.mjs +2110 -0
  58. package/dist/chunk-D25B5LZR.mjs +1739 -0
  59. package/dist/chunk-D3SIHB2V.mjs +2118 -0
  60. package/dist/chunk-FNPSK3CG.mjs +1760 -0
  61. package/dist/chunk-GXTGOLZT.mjs +92 -0
  62. package/dist/chunk-KDUUZQBK.mjs +1692 -0
  63. package/dist/chunk-KWIS5FQP.mjs +1739 -0
  64. package/dist/chunk-LERPI33Y.mjs +2060 -0
  65. package/dist/chunk-MZP3G7TF.mjs +2118 -0
  66. package/dist/chunk-NOHK5DLU.mjs +2173 -0
  67. package/dist/chunk-ORLC5Y4J.mjs +1787 -0
  68. package/dist/chunk-OTCQL7DY.mjs +2045 -0
  69. package/dist/chunk-RRB2C34Q.mjs +1738 -0
  70. package/dist/chunk-SFK6XTJE.mjs +2110 -0
  71. package/dist/chunk-U5R2FTCR.mjs +1803 -0
  72. package/dist/chunk-UU4HZ7ZT.mjs +1849 -0
  73. package/dist/chunk-WKOZOHOU.mjs +2060 -0
  74. package/dist/chunk-XIXAWCMS.mjs +1760 -0
  75. package/dist/chunk-XTAXUNQN.mjs +1742 -0
  76. package/dist/classifier.d.ts +114 -0
  77. package/dist/classifier.d.ts.map +1 -0
  78. package/dist/classifier.js +439 -0
  79. package/dist/classifier.js.map +1 -0
  80. package/dist/cli.js +681 -1170
  81. package/dist/cli.js.map +1 -1
  82. package/dist/cli.mjs +63 -533
  83. package/dist/cluster-detector.d.ts +8 -0
  84. package/dist/cluster-detector.d.ts.map +1 -0
  85. package/dist/cluster-detector.js +70 -0
  86. package/dist/cluster-detector.js.map +1 -0
  87. package/dist/defaults.d.ts +7 -0
  88. package/dist/defaults.d.ts.map +1 -0
  89. package/dist/defaults.js +54 -0
  90. package/dist/defaults.js.map +1 -0
  91. package/dist/graph-builder.d.ts +33 -0
  92. package/dist/graph-builder.d.ts.map +1 -0
  93. package/dist/graph-builder.js +225 -0
  94. package/dist/graph-builder.js.map +1 -0
  95. package/dist/index.d.mts +93 -106
  96. package/dist/index.d.ts +93 -106
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +932 -745
  99. package/dist/index.js.map +1 -1
  100. package/dist/index.mjs +262 -28
  101. package/dist/metrics.d.ts +34 -0
  102. package/dist/metrics.d.ts.map +1 -0
  103. package/dist/metrics.js +170 -0
  104. package/dist/metrics.js.map +1 -0
  105. package/dist/provider.d.ts +6 -0
  106. package/dist/provider.d.ts.map +1 -0
  107. package/dist/provider.js +48 -0
  108. package/dist/provider.js.map +1 -0
  109. package/dist/python-context-3GZKN3LR.mjs +162 -0
  110. package/dist/python-context-O2EN3M6Z.mjs +162 -0
  111. package/dist/remediation.d.ts +25 -0
  112. package/dist/remediation.d.ts.map +1 -0
  113. package/dist/remediation.js +98 -0
  114. package/dist/remediation.js.map +1 -0
  115. package/dist/scoring.d.ts +3 -7
  116. package/dist/scoring.d.ts.map +1 -1
  117. package/dist/scoring.js +57 -48
  118. package/dist/scoring.js.map +1 -1
  119. package/dist/semantic-analysis.d.ts +12 -23
  120. package/dist/semantic-analysis.d.ts.map +1 -1
  121. package/dist/semantic-analysis.js +172 -110
  122. package/dist/semantic-analysis.js.map +1 -1
  123. package/dist/summary.d.ts +6 -0
  124. package/dist/summary.d.ts.map +1 -0
  125. package/dist/summary.js +92 -0
  126. package/dist/summary.js.map +1 -0
  127. package/dist/types.d.ts +9 -2
  128. package/dist/types.d.ts.map +1 -1
  129. package/dist/utils/output-formatter.d.ts +14 -0
  130. package/dist/utils/output-formatter.d.ts.map +1 -0
  131. package/dist/utils/output-formatter.js +338 -0
  132. package/dist/utils/output-formatter.js.map +1 -0
  133. package/package.json +2 -2
  134. package/src/__tests__/analyzer.test.ts +1 -1
  135. package/src/__tests__/auto-detection.test.ts +1 -1
  136. package/src/__tests__/contract.test.ts +1 -1
  137. package/src/__tests__/enhanced-cohesion.test.ts +1 -1
  138. package/src/__tests__/file-classification.test.ts +1 -1
  139. package/src/__tests__/fragmentation-advanced.test.ts +1 -1
  140. package/src/__tests__/fragmentation-coupling.test.ts +1 -1
  141. package/src/__tests__/fragmentation-log.test.ts +1 -1
  142. package/src/__tests__/provider.test.ts +1 -1
  143. package/src/__tests__/structural-cohesion.test.ts +1 -1
  144. package/src/analyzer.ts +112 -317
  145. package/src/analyzers/python-context.ts +7 -76
  146. package/src/ast-utils.ts +2 -2
  147. package/src/classifier.ts +13 -328
  148. package/src/cli-action.ts +110 -0
  149. package/src/cli.ts +3 -701
  150. package/src/cluster-detector.ts +28 -1
  151. package/src/defaults.ts +3 -0
  152. package/src/graph-builder.ts +10 -91
  153. package/src/heuristics.ts +216 -0
  154. package/src/index.ts +6 -0
  155. package/src/issue-analyzer.ts +158 -0
  156. package/src/metrics.ts +9 -0
  157. package/src/scoring.ts +3 -5
  158. package/src/semantic-analysis.ts +8 -14
  159. package/src/summary.ts +62 -106
  160. package/src/types.ts +52 -20
  161. package/src/utils/dependency-graph-utils.ts +126 -0
  162. package/src/utils/output-formatter.ts +411 -0
  163. package/src/utils/string-utils.ts +21 -0
package/src/summary.ts CHANGED
@@ -3,15 +3,15 @@ import type {
3
3
  ContextSummary,
4
4
  ModuleCluster,
5
5
  } from './types';
6
- import { calculatePathEntropy, calculateDirectoryDistance } from './analyzer';
7
6
  import { GLOBAL_SCAN_OPTIONS } from '@aiready/core';
7
+ import { calculatePathEntropy } from './metrics';
8
8
 
9
9
  /**
10
10
  * Generate summary of context analysis results
11
11
  */
12
12
  export function generateSummary(
13
13
  results: ContextAnalysisResult[],
14
- options?: any
14
+ options: any = {}
15
15
  ): ContextSummary {
16
16
  const config = options
17
17
  ? Object.fromEntries(
@@ -19,137 +19,92 @@ export function generateSummary(
19
19
  ([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === 'rootDir'
20
20
  )
21
21
  )
22
- : undefined;
23
-
24
- if (results.length === 0) {
25
- return {
26
- totalFiles: 0,
27
- totalTokens: 0,
28
- avgContextBudget: 0,
29
- maxContextBudget: 0,
30
- avgImportDepth: 0,
31
- maxImportDepth: 0,
32
- deepFiles: [],
33
- avgFragmentation: 0,
34
- fragmentedModules: [],
35
- avgCohesion: 0,
36
- lowCohesionFiles: [],
37
- criticalIssues: 0,
38
- majorIssues: 0,
39
- minorIssues: 0,
40
- totalPotentialSavings: 0,
41
- topExpensiveFiles: [],
42
- config,
43
- };
44
- }
22
+ : {};
45
23
 
46
24
  const totalFiles = results.length;
47
25
  const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
48
- const totalContextBudget = results.reduce(
49
- (sum, r) => sum + r.contextBudget,
50
- 0
51
- );
52
- const avgContextBudget = totalContextBudget / totalFiles;
53
- const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
54
-
55
- const avgImportDepth =
56
- results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
57
- const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
26
+ const avgContextBudget =
27
+ totalFiles > 0
28
+ ? results.reduce((sum, r) => sum + r.contextBudget, 0) / totalFiles
29
+ : 0;
58
30
 
31
+ // Find deep files
59
32
  const deepFiles = results
60
- .filter((r) => r.importDepth >= 5)
61
- .map((r) => ({ file: r.file, depth: r.importDepth }))
62
- .sort((a, b) => b.depth - a.depth)
63
- .slice(0, 10);
33
+ .filter((r) => r.importDepth > 5)
34
+ .map((r) => ({ file: r.file, depth: r.importDepth }));
64
35
 
65
- const avgFragmentation =
66
- results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
36
+ const maxImportDepth = Math.max(0, ...results.map((r) => r.importDepth));
67
37
 
38
+ // Find fragmented modules (clusters)
68
39
  const moduleMap = new Map<string, ContextAnalysisResult[]>();
69
- for (const result of results) {
70
- for (const domain of result.domains) {
71
- if (!moduleMap.has(domain)) moduleMap.set(domain, []);
72
- moduleMap.get(domain)!.push(result);
40
+ results.forEach((r) => {
41
+ const parts = r.file.split('/');
42
+ // Try to identify domain/module (e.g., packages/core, src/utils)
43
+ let domain = 'root';
44
+ if (parts.length > 2) {
45
+ domain = parts.slice(0, 2).join('/');
73
46
  }
74
- }
47
+ if (!moduleMap.has(domain)) moduleMap.set(domain, []);
48
+ moduleMap.get(domain)!.push(r);
49
+ });
75
50
 
76
51
  const fragmentedModules: ModuleCluster[] = [];
77
- for (const [domain, files] of moduleMap.entries()) {
78
- if (files.length < 2) continue;
79
- const fragmentationScore =
80
- files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
81
- if (fragmentationScore < 0.3) continue;
82
-
83
- const totalTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
84
- const avgCohesion =
85
- files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
86
- const targetFiles = Math.max(1, Math.ceil(files.length / 3));
87
-
52
+ moduleMap.forEach((files, domain) => {
53
+ const clusterTokens = files.reduce((sum, f) => sum + f.tokenCost, 0);
88
54
  const filePaths = files.map((f) => f.file);
89
- const pathEntropy = calculatePathEntropy(filePaths);
90
- const directoryDistance = calculateDirectoryDistance(filePaths);
91
-
92
- function jaccard(a: string[], b: string[]) {
93
- const s1 = new Set(a || []);
94
- const s2 = new Set(b || []);
95
- if (s1.size === 0 && s2.size === 0) return 0;
96
- const inter = new Set([...s1].filter((x) => s2.has(x)));
97
- const uni = new Set([...s1, ...s2]);
98
- return uni.size === 0 ? 0 : inter.size / uni.size;
55
+ const avgEntropy = calculatePathEntropy(filePaths);
56
+
57
+ // A module is fragmented if it has many files with high directory distance
58
+ // and relatively low cohesion
59
+ const fragmentationScore = Math.min(1, avgEntropy * (files.length / 10));
60
+
61
+ if (fragmentationScore > 0.4) {
62
+ fragmentedModules.push({
63
+ domain,
64
+ files: filePaths,
65
+ fragmentationScore,
66
+ totalTokens: clusterTokens,
67
+ avgCohesion:
68
+ files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length,
69
+ suggestedStructure: {
70
+ targetFiles: Math.ceil(files.length / 2),
71
+ consolidationPlan: [
72
+ `Consolidate ${files.length} files in ${domain} into fewer modules`,
73
+ ],
74
+ },
75
+ });
99
76
  }
77
+ });
100
78
 
101
- let importSimTotal = 0;
102
- let importPairs = 0;
103
- for (let i = 0; i < files.length; i++) {
104
- for (let j = i + 1; j < files.length; j++) {
105
- importSimTotal += jaccard(
106
- files[i].dependencyList || [],
107
- files[j].dependencyList || []
108
- );
109
- importPairs++;
110
- }
111
- }
79
+ fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
112
80
 
113
- const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
114
-
115
- fragmentedModules.push({
116
- domain,
117
- files: files.map((f) => f.file),
118
- totalTokens,
119
- fragmentationScore,
120
- avgCohesion,
121
- importCohesion,
122
- pathEntropy,
123
- directoryDistance,
124
- suggestedStructure: {
125
- targetFiles,
126
- consolidationPlan: [
127
- `Consolidate ${files.length} files across ${new Set(files.map((f) => f.file.split('/').slice(0, -1).join('/'))).size} directories`,
128
- `Target ~${targetFiles} core modules to reduce context switching`,
129
- ],
130
- },
131
- });
132
- }
81
+ const avgFragmentation =
82
+ fragmentedModules.length > 0
83
+ ? fragmentedModules.reduce((sum, m) => sum + m.fragmentationScore, 0) /
84
+ fragmentedModules.length
85
+ : 0;
133
86
 
87
+ // Cohesion
134
88
  const avgCohesion =
135
- results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
89
+ results.reduce((sum, r) => sum + r.cohesionScore, 0) / (totalFiles || 1);
90
+
136
91
  const lowCohesionFiles = results
137
92
  .filter((r) => r.cohesionScore < 0.4)
138
- .map((r) => ({ file: r.file, score: r.cohesionScore }))
139
- .sort((a, b) => a.score - b.score)
140
- .slice(0, 10);
93
+ .map((r) => ({ file: r.file, score: r.cohesionScore }));
141
94
 
95
+ // Issues
142
96
  const criticalIssues = results.filter(
143
97
  (r) => r.severity === 'critical'
144
98
  ).length;
145
99
  const majorIssues = results.filter((r) => r.severity === 'major').length;
146
100
  const minorIssues = results.filter((r) => r.severity === 'minor').length;
101
+
147
102
  const totalPotentialSavings = results.reduce(
148
- (sum, r) => sum + r.potentialSavings,
103
+ (sum, r) => sum + (r.potentialSavings || 0),
149
104
  0
150
105
  );
151
106
 
152
- const topExpensiveFiles = results
107
+ const topExpensiveFiles = [...results]
153
108
  .sort((a, b) => b.contextBudget - a.contextBudget)
154
109
  .slice(0, 10)
155
110
  .map((r) => ({
@@ -162,8 +117,9 @@ export function generateSummary(
162
117
  totalFiles,
163
118
  totalTokens,
164
119
  avgContextBudget,
165
- maxContextBudget,
166
- avgImportDepth,
120
+ maxContextBudget: Math.max(0, ...results.map((r) => r.contextBudget)),
121
+ avgImportDepth:
122
+ results.reduce((sum, r) => sum + r.importDepth, 0) / (totalFiles || 1),
167
123
  maxImportDepth,
168
124
  deepFiles,
169
125
  avgFragmentation,
package/src/types.ts CHANGED
@@ -1,45 +1,77 @@
1
1
  import type { ScanOptions, Severity } from '@aiready/core';
2
2
 
3
+ /**
4
+ * Options for the Context Analyzer tool.
5
+ * Controls thresholds for import depth, context budget, and cohesion.
6
+ */
3
7
  export interface ContextAnalyzerOptions extends ScanOptions {
4
- maxDepth?: number; // Maximum acceptable import depth, default 5
5
- maxContextBudget?: number; // Maximum acceptable token budget, default 10000
6
- minCohesion?: number; // Minimum acceptable cohesion score (0-1), default 0.6
7
- maxFragmentation?: number; // Maximum acceptable fragmentation (0-1), default 0.5
8
- focus?: 'fragmentation' | 'cohesion' | 'depth' | 'all'; // Analysis focus, default 'all'
9
- includeNodeModules?: boolean; // Include node_modules in analysis, default false
8
+ /** Maximum acceptable import depth (default: 5) */
9
+ maxDepth?: number;
10
+ /** Maximum acceptable token budget for a single context (default: 25000) */
11
+ maxContextBudget?: number;
12
+ /** Minimum acceptable cohesion score between 0 and 1 (default: 0.6) */
13
+ minCohesion?: number;
14
+ /** Maximum acceptable fragmentation score between 0 and 1 (default: 0.5) */
15
+ maxFragmentation?: number;
16
+ /** Analysis focus area: fragmentation, cohesion, depth, or all (default: 'all') */
17
+ focus?: 'fragmentation' | 'cohesion' | 'depth' | 'all';
18
+ /** Whether to include node_modules in the analysis (default: false) */
19
+ includeNodeModules?: boolean;
10
20
  }
11
21
 
22
+ /**
23
+ * The result of a context analysis for a single file or module.
24
+ * Includes metrics for tokens, dependencies, cohesion, and AI impact.
25
+ */
12
26
  export interface ContextAnalysisResult {
27
+ /** The file path being analyzed */
13
28
  file: string;
14
29
 
15
30
  // Basic metrics
16
- tokenCost: number; // Total tokens in this file
31
+ /** Total number of tokens in this file */
32
+ tokenCost: number;
33
+ /** Total lines of code in the file */
17
34
  linesOfCode: number;
18
35
 
19
36
  // Dependency analysis
20
- importDepth: number; // Max depth of import tree
21
- dependencyCount: number; // Total transitive dependencies
22
- dependencyList: string[]; // All files in dependency tree
23
- circularDeps: string[][]; // Circular dependency chains if any
37
+ /** Maximum depth of the import tree for this file */
38
+ importDepth: number;
39
+ /** Total number of transitive dependencies */
40
+ dependencyCount: number;
41
+ /** List of all files in the dependency tree */
42
+ dependencyList: string[];
43
+ /** Detected circular dependency chains */
44
+ circularDeps: string[][];
24
45
 
25
46
  // Cohesion analysis
26
- cohesionScore: number; // 0-1, how related are exports (1 = perfect cohesion)
27
- domains: string[]; // Detected domain categories (e.g., ['user', 'auth'])
47
+ /** Cohesion score from 0 to 1 (1 is perfect cohesion) */
48
+ cohesionScore: number;
49
+ /** Detected domain categories for the module */
50
+ domains: string[];
51
+ /** Number of exported symbols */
28
52
  exportCount: number;
29
53
 
30
54
  // AI context impact
31
- contextBudget: number; // Total tokens to understand this file (includes all deps)
32
- fragmentationScore: number; // 0-1, how scattered is this domain (0 = well-grouped)
33
- relatedFiles: string[]; // Files that should be loaded together
55
+ /** Total tokens required to understand this file and all its dependencies */
56
+ contextBudget: number;
57
+ /** Fragmentation score from 0 to 1 (0 is well-grouped) */
58
+ fragmentationScore: number;
59
+ /** List of files that should be loaded together for full context */
60
+ relatedFiles: string[];
34
61
 
35
62
  // File classification (NEW)
36
- fileClassification: FileClassification; // Type of file for analysis context
63
+ /** The semantic classification of the file (e.g. 'barrel-export', 'service-file') */
64
+ fileClassification: FileClassification;
37
65
 
38
66
  // Recommendations
67
+ /** Overall severity of identified issues */
39
68
  severity: Severity | 'critical' | 'major' | 'minor' | 'info';
40
- issues: string[]; // List of specific problems
41
- recommendations: string[]; // Actionable suggestions
42
- potentialSavings: number; // Estimated token savings if fixed
69
+ /** List of specific structural problems found */
70
+ issues: string[];
71
+ /** Actionable suggestions for improving context readiness */
72
+ recommendations: string[];
73
+ /** Estimated tokens that could be saved by following recommendations */
74
+ potentialSavings: number;
43
75
  }
44
76
 
45
77
  /**
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Shared dependency graph utilities used by context analyzers.
3
+ */
4
+
5
+ export function calculateImportDepthFromEdges(
6
+ file: string,
7
+ edges: Map<string, Set<string>>,
8
+ visited = new Set<string>(),
9
+ depth = 0
10
+ ): number {
11
+ if (visited.has(file)) return depth;
12
+
13
+ const dependencies = edges.get(file);
14
+ if (!dependencies || dependencies.size === 0) return depth;
15
+
16
+ const nextVisited = new Set(visited);
17
+ nextVisited.add(file);
18
+
19
+ let maxDepth = depth;
20
+ for (const dep of dependencies) {
21
+ maxDepth = Math.max(
22
+ maxDepth,
23
+ calculateImportDepthFromEdges(dep, edges, nextVisited, depth + 1)
24
+ );
25
+ }
26
+
27
+ return maxDepth;
28
+ }
29
+
30
+ export function getTransitiveDependenciesFromEdges(
31
+ file: string,
32
+ edges: Map<string, Set<string>>,
33
+ visited = new Set<string>()
34
+ ): string[] {
35
+ if (visited.has(file)) return [];
36
+
37
+ const nextVisited = new Set(visited);
38
+ nextVisited.add(file);
39
+
40
+ const dependencies = edges.get(file);
41
+ if (!dependencies || dependencies.size === 0) return [];
42
+
43
+ const allDeps: string[] = [];
44
+ for (const dep of dependencies) {
45
+ allDeps.push(dep);
46
+ allDeps.push(
47
+ ...getTransitiveDependenciesFromEdges(dep, edges, nextVisited)
48
+ );
49
+ }
50
+
51
+ return [...new Set(allDeps)];
52
+ }
53
+
54
+ export function detectGraphCycles(edges: Map<string, Set<string>>): string[][] {
55
+ const cycles: string[][] = [];
56
+ const visited = new Set<string>();
57
+ const recursionStack = new Set<string>();
58
+
59
+ function dfs(file: string, path: string[]): void {
60
+ if (recursionStack.has(file)) {
61
+ const cycleStart = path.indexOf(file);
62
+ if (cycleStart !== -1) {
63
+ cycles.push([...path.slice(cycleStart), file]);
64
+ }
65
+ return;
66
+ }
67
+
68
+ if (visited.has(file)) return;
69
+
70
+ visited.add(file);
71
+ recursionStack.add(file);
72
+
73
+ const dependencies = edges.get(file);
74
+ if (dependencies) {
75
+ for (const dep of dependencies) {
76
+ dfs(dep, [...path, file]);
77
+ }
78
+ }
79
+
80
+ recursionStack.delete(file);
81
+ }
82
+
83
+ for (const file of edges.keys()) {
84
+ if (!visited.has(file)) {
85
+ dfs(file, []);
86
+ }
87
+ }
88
+
89
+ return cycles;
90
+ }
91
+
92
+ export function detectGraphCyclesFromFile(
93
+ file: string,
94
+ edges: Map<string, Set<string>>
95
+ ): string[][] {
96
+ const cycles: string[][] = [];
97
+ const visited = new Set<string>();
98
+ const recursionStack = new Set<string>();
99
+
100
+ function dfs(current: string, path: string[]): void {
101
+ if (recursionStack.has(current)) {
102
+ const cycleStart = path.indexOf(current);
103
+ if (cycleStart !== -1) {
104
+ cycles.push([...path.slice(cycleStart), current]);
105
+ }
106
+ return;
107
+ }
108
+
109
+ if (visited.has(current)) return;
110
+
111
+ visited.add(current);
112
+ recursionStack.add(current);
113
+
114
+ const dependencies = edges.get(current);
115
+ if (dependencies) {
116
+ for (const dep of dependencies) {
117
+ dfs(dep, [...path, current]);
118
+ }
119
+ }
120
+
121
+ recursionStack.delete(current);
122
+ }
123
+
124
+ dfs(file, []);
125
+ return cycles;
126
+ }