tlc-claude-code 1.2.29 → 1.4.0

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 (182) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  14. package/dashboard/dist/components/UsagePane.js +51 -0
  15. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  16. package/dashboard/dist/components/UsagePane.test.js +142 -0
  17. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  18. package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
  19. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  20. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  21. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  22. package/dashboard/dist/components/WorkspacePane.js +17 -0
  23. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  24. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  25. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  26. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  27. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  28. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  29. package/package.json +1 -1
  30. package/server/lib/access-control-doc.js +541 -0
  31. package/server/lib/access-control-doc.test.js +672 -0
  32. package/server/lib/adr-generator.js +423 -0
  33. package/server/lib/adr-generator.test.js +586 -0
  34. package/server/lib/agent-progress-monitor.js +223 -0
  35. package/server/lib/agent-progress-monitor.test.js +202 -0
  36. package/server/lib/architecture-command.js +450 -0
  37. package/server/lib/architecture-command.test.js +754 -0
  38. package/server/lib/ast-analyzer.js +324 -0
  39. package/server/lib/ast-analyzer.test.js +437 -0
  40. package/server/lib/audit-attribution.js +191 -0
  41. package/server/lib/audit-attribution.test.js +359 -0
  42. package/server/lib/audit-classifier.js +202 -0
  43. package/server/lib/audit-classifier.test.js +209 -0
  44. package/server/lib/audit-command.js +275 -0
  45. package/server/lib/audit-command.test.js +325 -0
  46. package/server/lib/audit-exporter.js +380 -0
  47. package/server/lib/audit-exporter.test.js +464 -0
  48. package/server/lib/audit-logger.js +236 -0
  49. package/server/lib/audit-logger.test.js +364 -0
  50. package/server/lib/audit-query.js +257 -0
  51. package/server/lib/audit-query.test.js +352 -0
  52. package/server/lib/audit-storage.js +269 -0
  53. package/server/lib/audit-storage.test.js +272 -0
  54. package/server/lib/auth-system.test.js +4 -1
  55. package/server/lib/boundary-detector.js +427 -0
  56. package/server/lib/boundary-detector.test.js +320 -0
  57. package/server/lib/budget-alerts.js +138 -0
  58. package/server/lib/budget-alerts.test.js +235 -0
  59. package/server/lib/bulk-repo-init.js +342 -0
  60. package/server/lib/bulk-repo-init.test.js +388 -0
  61. package/server/lib/candidates-tracker.js +210 -0
  62. package/server/lib/candidates-tracker.test.js +300 -0
  63. package/server/lib/checkpoint-manager.js +251 -0
  64. package/server/lib/checkpoint-manager.test.js +474 -0
  65. package/server/lib/circular-detector.js +337 -0
  66. package/server/lib/circular-detector.test.js +353 -0
  67. package/server/lib/cohesion-analyzer.js +310 -0
  68. package/server/lib/cohesion-analyzer.test.js +447 -0
  69. package/server/lib/compliance-checklist.js +866 -0
  70. package/server/lib/compliance-checklist.test.js +476 -0
  71. package/server/lib/compliance-command.js +616 -0
  72. package/server/lib/compliance-command.test.js +551 -0
  73. package/server/lib/compliance-reporter.js +692 -0
  74. package/server/lib/compliance-reporter.test.js +707 -0
  75. package/server/lib/contract-testing.js +625 -0
  76. package/server/lib/contract-testing.test.js +342 -0
  77. package/server/lib/conversion-planner.js +469 -0
  78. package/server/lib/conversion-planner.test.js +361 -0
  79. package/server/lib/convert-command.js +351 -0
  80. package/server/lib/convert-command.test.js +608 -0
  81. package/server/lib/coupling-calculator.js +189 -0
  82. package/server/lib/coupling-calculator.test.js +509 -0
  83. package/server/lib/data-flow-doc.js +665 -0
  84. package/server/lib/data-flow-doc.test.js +659 -0
  85. package/server/lib/dependency-graph.js +367 -0
  86. package/server/lib/dependency-graph.test.js +516 -0
  87. package/server/lib/duplication-detector.js +349 -0
  88. package/server/lib/duplication-detector.test.js +401 -0
  89. package/server/lib/ephemeral-storage.js +249 -0
  90. package/server/lib/ephemeral-storage.test.js +254 -0
  91. package/server/lib/evidence-collector.js +627 -0
  92. package/server/lib/evidence-collector.test.js +901 -0
  93. package/server/lib/example-service.js +616 -0
  94. package/server/lib/example-service.test.js +397 -0
  95. package/server/lib/flow-diagram-generator.js +474 -0
  96. package/server/lib/flow-diagram-generator.test.js +446 -0
  97. package/server/lib/idp-manager.js +626 -0
  98. package/server/lib/idp-manager.test.js +587 -0
  99. package/server/lib/impact-scorer.js +184 -0
  100. package/server/lib/impact-scorer.test.js +211 -0
  101. package/server/lib/memory-exclusion.js +326 -0
  102. package/server/lib/memory-exclusion.test.js +241 -0
  103. package/server/lib/mermaid-generator.js +358 -0
  104. package/server/lib/mermaid-generator.test.js +301 -0
  105. package/server/lib/messaging-patterns.js +750 -0
  106. package/server/lib/messaging-patterns.test.js +213 -0
  107. package/server/lib/mfa-handler.js +452 -0
  108. package/server/lib/mfa-handler.test.js +490 -0
  109. package/server/lib/microservice-template.js +386 -0
  110. package/server/lib/microservice-template.test.js +325 -0
  111. package/server/lib/new-project-microservice.js +450 -0
  112. package/server/lib/new-project-microservice.test.js +600 -0
  113. package/server/lib/oauth-flow.js +375 -0
  114. package/server/lib/oauth-flow.test.js +487 -0
  115. package/server/lib/oauth-registry.js +190 -0
  116. package/server/lib/oauth-registry.test.js +306 -0
  117. package/server/lib/readme-generator.js +490 -0
  118. package/server/lib/readme-generator.test.js +493 -0
  119. package/server/lib/refactor-command.js +326 -0
  120. package/server/lib/refactor-command.test.js +528 -0
  121. package/server/lib/refactor-executor.js +254 -0
  122. package/server/lib/refactor-executor.test.js +305 -0
  123. package/server/lib/refactor-observer.js +292 -0
  124. package/server/lib/refactor-observer.test.js +422 -0
  125. package/server/lib/refactor-progress.js +193 -0
  126. package/server/lib/refactor-progress.test.js +251 -0
  127. package/server/lib/refactor-reporter.js +237 -0
  128. package/server/lib/refactor-reporter.test.js +247 -0
  129. package/server/lib/repo-dependency-tracker.js +261 -0
  130. package/server/lib/repo-dependency-tracker.test.js +350 -0
  131. package/server/lib/retention-policy.js +281 -0
  132. package/server/lib/retention-policy.test.js +486 -0
  133. package/server/lib/role-mapper.js +236 -0
  134. package/server/lib/role-mapper.test.js +395 -0
  135. package/server/lib/saml-provider.js +765 -0
  136. package/server/lib/saml-provider.test.js +643 -0
  137. package/server/lib/security-policy-generator.js +682 -0
  138. package/server/lib/security-policy-generator.test.js +544 -0
  139. package/server/lib/semantic-analyzer.js +198 -0
  140. package/server/lib/semantic-analyzer.test.js +474 -0
  141. package/server/lib/sensitive-detector.js +112 -0
  142. package/server/lib/sensitive-detector.test.js +209 -0
  143. package/server/lib/service-interaction-diagram.js +700 -0
  144. package/server/lib/service-interaction-diagram.test.js +638 -0
  145. package/server/lib/service-scaffold.js +486 -0
  146. package/server/lib/service-scaffold.test.js +373 -0
  147. package/server/lib/service-summary.js +553 -0
  148. package/server/lib/service-summary.test.js +619 -0
  149. package/server/lib/session-purge.js +460 -0
  150. package/server/lib/session-purge.test.js +312 -0
  151. package/server/lib/shared-kernel.js +578 -0
  152. package/server/lib/shared-kernel.test.js +255 -0
  153. package/server/lib/sso-command.js +544 -0
  154. package/server/lib/sso-command.test.js +552 -0
  155. package/server/lib/sso-session.js +492 -0
  156. package/server/lib/sso-session.test.js +670 -0
  157. package/server/lib/traefik-config.js +282 -0
  158. package/server/lib/traefik-config.test.js +312 -0
  159. package/server/lib/usage-command.js +218 -0
  160. package/server/lib/usage-command.test.js +391 -0
  161. package/server/lib/usage-formatter.js +192 -0
  162. package/server/lib/usage-formatter.test.js +267 -0
  163. package/server/lib/usage-history.js +122 -0
  164. package/server/lib/usage-history.test.js +206 -0
  165. package/server/lib/workspace-command.js +249 -0
  166. package/server/lib/workspace-command.test.js +264 -0
  167. package/server/lib/workspace-config.js +270 -0
  168. package/server/lib/workspace-config.test.js +312 -0
  169. package/server/lib/workspace-docs-command.js +547 -0
  170. package/server/lib/workspace-docs-command.test.js +692 -0
  171. package/server/lib/workspace-memory.js +451 -0
  172. package/server/lib/workspace-memory.test.js +403 -0
  173. package/server/lib/workspace-scanner.js +452 -0
  174. package/server/lib/workspace-scanner.test.js +677 -0
  175. package/server/lib/workspace-test-runner.js +315 -0
  176. package/server/lib/workspace-test-runner.test.js +294 -0
  177. package/server/lib/zero-retention-command.js +439 -0
  178. package/server/lib/zero-retention-command.test.js +448 -0
  179. package/server/lib/zero-retention.js +322 -0
  180. package/server/lib/zero-retention.test.js +258 -0
  181. package/server/package-lock.json +14 -0
  182. package/server/package.json +1 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Coupling Calculator
3
+ * Calculate coupling metrics for dependency graphs
4
+ */
5
+
6
+ class CouplingCalculator {
7
+ /**
8
+ * Create a coupling calculator
9
+ * @param {DependencyGraph} graph - The dependency graph instance
10
+ */
11
+ constructor(graph) {
12
+ this.graph = graph;
13
+ }
14
+
15
+ /**
16
+ * Get afferent coupling (Ca) - number of files that depend on this file
17
+ * @param {string} filePath - Absolute path to the file
18
+ * @returns {number} Number of incoming dependencies
19
+ */
20
+ getAfferentCoupling(filePath) {
21
+ const importers = this.graph.getImporters(filePath);
22
+ return importers.length;
23
+ }
24
+
25
+ /**
26
+ * Get efferent coupling (Ce) - number of files this file depends on
27
+ * @param {string} filePath - Absolute path to the file
28
+ * @returns {number} Number of outgoing dependencies
29
+ */
30
+ getEfferentCoupling(filePath) {
31
+ const imports = this.graph.getImports(filePath);
32
+ return imports.length;
33
+ }
34
+
35
+ /**
36
+ * Get instability ratio: Ce / (Ca + Ce)
37
+ * 0 = maximally stable (only depended upon, doesn't depend)
38
+ * 1 = maximally unstable (only depends, not depended upon)
39
+ * @param {string} filePath - Absolute path to the file
40
+ * @returns {number} Instability ratio between 0 and 1
41
+ */
42
+ getInstability(filePath) {
43
+ const ca = this.getAfferentCoupling(filePath);
44
+ const ce = this.getEfferentCoupling(filePath);
45
+
46
+ if (ca + ce === 0) {
47
+ return 0; // Isolated file has 0 instability
48
+ }
49
+
50
+ return ce / (ca + ce);
51
+ }
52
+
53
+ /**
54
+ * Get hub files - files with high afferent coupling (many dependents)
55
+ * @param {Object} options - Options
56
+ * @param {number} options.threshold - Minimum afferent coupling to be considered a hub
57
+ * @returns {Array<{file: string, afferentCoupling: number}>} Hub files sorted by coupling
58
+ */
59
+ getHubFiles(options = {}) {
60
+ const { threshold = 3 } = options;
61
+ const files = this.graph.getFiles();
62
+ const hubs = [];
63
+
64
+ for (const file of files) {
65
+ const ca = this.getAfferentCoupling(file);
66
+ if (ca >= threshold) {
67
+ hubs.push({
68
+ file,
69
+ afferentCoupling: ca,
70
+ });
71
+ }
72
+ }
73
+
74
+ return hubs.sort((a, b) => b.afferentCoupling - a.afferentCoupling);
75
+ }
76
+
77
+ /**
78
+ * Get dependent files - files with high efferent coupling (many dependencies)
79
+ * @param {Object} options - Options
80
+ * @param {number} options.threshold - Minimum efferent coupling to be considered dependent
81
+ * @returns {Array<{file: string, efferentCoupling: number}>} Dependent files sorted by coupling
82
+ */
83
+ getDependentFiles(options = {}) {
84
+ const { threshold = 3 } = options;
85
+ const files = this.graph.getFiles();
86
+ const dependent = [];
87
+
88
+ for (const file of files) {
89
+ const ce = this.getEfferentCoupling(file);
90
+ if (ce >= threshold) {
91
+ dependent.push({
92
+ file,
93
+ efferentCoupling: ce,
94
+ });
95
+ }
96
+ }
97
+
98
+ return dependent.sort((a, b) => b.efferentCoupling - a.efferentCoupling);
99
+ }
100
+
101
+ /**
102
+ * Get isolated files - files with no coupling (neither imports nor is imported)
103
+ * @returns {Array<string>} Array of isolated file paths
104
+ */
105
+ getIsolatedFiles() {
106
+ const files = this.graph.getFiles();
107
+ const isolated = [];
108
+
109
+ for (const file of files) {
110
+ const ca = this.getAfferentCoupling(file);
111
+ const ce = this.getEfferentCoupling(file);
112
+ if (ca === 0 && ce === 0) {
113
+ isolated.push(file);
114
+ }
115
+ }
116
+
117
+ return isolated;
118
+ }
119
+
120
+ /**
121
+ * Get highly coupled modules - files with high total coupling (Ca + Ce)
122
+ * @param {Object} options - Options
123
+ * @param {number} options.threshold - Minimum total coupling to be considered highly coupled
124
+ * @returns {Array<{file: string, afferentCoupling: number, efferentCoupling: number, totalCoupling: number}>}
125
+ */
126
+ getHighlyCoupledModules(options = {}) {
127
+ const { threshold = 4 } = options;
128
+ const files = this.graph.getFiles();
129
+ const highlyCoupled = [];
130
+
131
+ for (const file of files) {
132
+ const ca = this.getAfferentCoupling(file);
133
+ const ce = this.getEfferentCoupling(file);
134
+ const total = ca + ce;
135
+
136
+ if (total >= threshold) {
137
+ highlyCoupled.push({
138
+ file,
139
+ afferentCoupling: ca,
140
+ efferentCoupling: ce,
141
+ totalCoupling: total,
142
+ });
143
+ }
144
+ }
145
+
146
+ return highlyCoupled.sort((a, b) => b.totalCoupling - a.totalCoupling);
147
+ }
148
+
149
+ /**
150
+ * Get coupling metrics for all files in the graph
151
+ * @returns {Array<{file: string, afferentCoupling: number, efferentCoupling: number, instability: number}>}
152
+ */
153
+ getAllMetrics() {
154
+ const files = this.graph.getFiles();
155
+ return files.map(file => ({
156
+ file,
157
+ afferentCoupling: this.getAfferentCoupling(file),
158
+ efferentCoupling: this.getEfferentCoupling(file),
159
+ instability: this.getInstability(file),
160
+ }));
161
+ }
162
+
163
+ /**
164
+ * Generate coupling matrix - shows which files depend on which
165
+ * @returns {{files: Array<string>, matrix: Array<Array<number>>}} Matrix where matrix[i][j] = 1 if files[i] imports files[j]
166
+ */
167
+ getCouplingMatrix() {
168
+ const files = this.graph.getFiles();
169
+ const n = files.length;
170
+ const matrix = Array(n).fill(null).map(() => Array(n).fill(0));
171
+
172
+ for (let i = 0; i < n; i++) {
173
+ const imports = this.graph.getImports(files[i]);
174
+ for (const imp of imports) {
175
+ const j = files.indexOf(imp);
176
+ if (j !== -1) {
177
+ matrix[i][j] = 1;
178
+ }
179
+ }
180
+ }
181
+
182
+ return {
183
+ files,
184
+ matrix,
185
+ };
186
+ }
187
+ }
188
+
189
+ module.exports = { CouplingCalculator };
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Coupling Calculator Tests
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ describe('CouplingCalculator', () => {
8
+ describe('afferent coupling (Ca)', () => {
9
+ it('calculates afferent coupling correctly', async () => {
10
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
11
+ const { DependencyGraph } = await import('./dependency-graph.js');
12
+
13
+ const graph = new DependencyGraph({
14
+ basePath: '/project',
15
+ fileExists: () => true,
16
+ readFile: vi.fn().mockImplementation((path) => {
17
+ if (path === '/project/src/index.js') return `import './utils';`;
18
+ if (path === '/project/src/app.js') return `import './utils';`;
19
+ if (path === '/project/src/service.js') return `import './utils';`;
20
+ return '';
21
+ }),
22
+ });
23
+
24
+ await graph.build('/project/src/index.js');
25
+ await graph.build('/project/src/app.js');
26
+ await graph.build('/project/src/service.js');
27
+
28
+ const calculator = new CouplingCalculator(graph);
29
+ const ca = calculator.getAfferentCoupling('/project/src/utils.js');
30
+
31
+ expect(ca).toBe(3); // 3 files depend on utils.js
32
+ });
33
+
34
+ it('returns 0 for files with no dependents', async () => {
35
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
36
+ const { DependencyGraph } = await import('./dependency-graph.js');
37
+
38
+ const graph = new DependencyGraph({
39
+ basePath: '/project',
40
+ fileExists: () => true,
41
+ readFile: vi.fn().mockImplementation((path) => {
42
+ if (path === '/project/src/index.js') return `import './utils';`;
43
+ return '';
44
+ }),
45
+ });
46
+
47
+ await graph.build('/project/src/index.js');
48
+
49
+ const calculator = new CouplingCalculator(graph);
50
+ const ca = calculator.getAfferentCoupling('/project/src/index.js');
51
+
52
+ expect(ca).toBe(0); // index.js has no dependents
53
+ });
54
+ });
55
+
56
+ describe('efferent coupling (Ce)', () => {
57
+ it('calculates efferent coupling correctly', async () => {
58
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
59
+ const { DependencyGraph } = await import('./dependency-graph.js');
60
+
61
+ const graph = new DependencyGraph({
62
+ basePath: '/project',
63
+ fileExists: () => true,
64
+ readFile: vi.fn().mockImplementation((path) => {
65
+ if (path === '/project/src/index.js') {
66
+ return `
67
+ import './utils';
68
+ import './config';
69
+ import './helpers';
70
+ `;
71
+ }
72
+ return '';
73
+ }),
74
+ });
75
+
76
+ await graph.build('/project/src/index.js');
77
+
78
+ const calculator = new CouplingCalculator(graph);
79
+ const ce = calculator.getEfferentCoupling('/project/src/index.js');
80
+
81
+ expect(ce).toBe(3); // index.js depends on 3 files
82
+ });
83
+
84
+ it('returns 0 for files with no dependencies', async () => {
85
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
86
+ const { DependencyGraph } = await import('./dependency-graph.js');
87
+
88
+ const graph = new DependencyGraph({
89
+ basePath: '/project',
90
+ fileExists: () => true,
91
+ readFile: vi.fn().mockImplementation((path) => {
92
+ if (path === '/project/src/index.js') return `import './utils';`;
93
+ return '';
94
+ }),
95
+ });
96
+
97
+ await graph.build('/project/src/index.js');
98
+
99
+ const calculator = new CouplingCalculator(graph);
100
+ const ce = calculator.getEfferentCoupling('/project/src/utils.js');
101
+
102
+ expect(ce).toBe(0); // utils.js has no dependencies
103
+ });
104
+ });
105
+
106
+ describe('instability ratio', () => {
107
+ it('calculates instability ratio Ce / (Ca + Ce)', async () => {
108
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
109
+ const { DependencyGraph } = await import('./dependency-graph.js');
110
+
111
+ const graph = new DependencyGraph({
112
+ basePath: '/project',
113
+ fileExists: () => true,
114
+ readFile: vi.fn().mockImplementation((path) => {
115
+ // Simple chain: index -> utils -> config
116
+ // utils has Ca=1 (index), Ce=1 (config)
117
+ if (path === '/project/src/index.js') return `import './utils';`;
118
+ if (path === '/project/src/utils.js') return `import './config';`;
119
+ return '';
120
+ }),
121
+ });
122
+
123
+ await graph.build('/project/src/index.js');
124
+
125
+ const calculator = new CouplingCalculator(graph);
126
+ const instability = calculator.getInstability('/project/src/utils.js');
127
+
128
+ // Ce = 1, Ca = 1, Instability = 1 / (1 + 1) = 0.5
129
+ expect(instability).toBe(0.5);
130
+ });
131
+
132
+ it('returns 0 for files with no coupling (isolated)', async () => {
133
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
134
+ const { DependencyGraph } = await import('./dependency-graph.js');
135
+
136
+ const graph = new DependencyGraph({
137
+ basePath: '/project',
138
+ fileExists: () => true,
139
+ readFile: vi.fn().mockResolvedValue(''),
140
+ });
141
+
142
+ await graph.build('/project/src/isolated.js');
143
+
144
+ const calculator = new CouplingCalculator(graph);
145
+ const instability = calculator.getInstability('/project/src/isolated.js');
146
+
147
+ expect(instability).toBe(0); // No coupling means 0 instability
148
+ });
149
+
150
+ it('returns 1 for files that only have efferent coupling', async () => {
151
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
152
+ const { DependencyGraph } = await import('./dependency-graph.js');
153
+
154
+ const graph = new DependencyGraph({
155
+ basePath: '/project',
156
+ fileExists: () => true,
157
+ readFile: vi.fn().mockImplementation((path) => {
158
+ if (path === '/project/src/index.js') {
159
+ return `import './utils';`;
160
+ }
161
+ return '';
162
+ }),
163
+ });
164
+
165
+ await graph.build('/project/src/index.js');
166
+
167
+ const calculator = new CouplingCalculator(graph);
168
+ const instability = calculator.getInstability('/project/src/index.js');
169
+
170
+ // Ce = 1, Ca = 0, Instability = 1 / (0 + 1) = 1
171
+ expect(instability).toBe(1);
172
+ });
173
+
174
+ it('returns 0 for files that only have afferent coupling', async () => {
175
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
176
+ const { DependencyGraph } = await import('./dependency-graph.js');
177
+
178
+ const graph = new DependencyGraph({
179
+ basePath: '/project',
180
+ fileExists: () => true,
181
+ readFile: vi.fn().mockImplementation((path) => {
182
+ if (path === '/project/src/index.js') return `import './utils';`;
183
+ if (path === '/project/src/app.js') return `import './utils';`;
184
+ return '';
185
+ }),
186
+ });
187
+
188
+ await graph.build('/project/src/index.js');
189
+ await graph.build('/project/src/app.js');
190
+
191
+ const calculator = new CouplingCalculator(graph);
192
+ const instability = calculator.getInstability('/project/src/utils.js');
193
+
194
+ // Ce = 0, Ca = 2, Instability = 0 / (2 + 0) = 0
195
+ expect(instability).toBe(0);
196
+ });
197
+ });
198
+
199
+ describe('hub files (high afferent)', () => {
200
+ it('identifies hub files with high afferent coupling', async () => {
201
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
202
+ const { DependencyGraph } = await import('./dependency-graph.js');
203
+
204
+ const graph = new DependencyGraph({
205
+ basePath: '/project',
206
+ fileExists: () => true,
207
+ readFile: vi.fn().mockImplementation((path) => {
208
+ // utils.js is imported by 5 files (hub)
209
+ if (path === '/project/src/a.js') return `import './utils';`;
210
+ if (path === '/project/src/b.js') return `import './utils';`;
211
+ if (path === '/project/src/c.js') return `import './utils';`;
212
+ if (path === '/project/src/d.js') return `import './utils';`;
213
+ if (path === '/project/src/e.js') return `import './utils';`;
214
+ // config.js is imported by 1 file (not a hub)
215
+ if (path === '/project/src/f.js') return `import './config';`;
216
+ return '';
217
+ }),
218
+ });
219
+
220
+ await graph.build('/project/src/a.js');
221
+ await graph.build('/project/src/b.js');
222
+ await graph.build('/project/src/c.js');
223
+ await graph.build('/project/src/d.js');
224
+ await graph.build('/project/src/e.js');
225
+ await graph.build('/project/src/f.js');
226
+
227
+ const calculator = new CouplingCalculator(graph);
228
+ const hubs = calculator.getHubFiles({ threshold: 3 });
229
+
230
+ expect(hubs).toHaveLength(1);
231
+ expect(hubs[0].file).toBe('/project/src/utils.js');
232
+ expect(hubs[0].afferentCoupling).toBe(5);
233
+ });
234
+
235
+ it('returns empty array when no hub files exist', async () => {
236
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
237
+ const { DependencyGraph } = await import('./dependency-graph.js');
238
+
239
+ const graph = new DependencyGraph({
240
+ basePath: '/project',
241
+ fileExists: () => true,
242
+ readFile: vi.fn().mockImplementation((path) => {
243
+ if (path === '/project/src/a.js') return `import './b';`;
244
+ if (path === '/project/src/c.js') return `import './d';`;
245
+ return '';
246
+ }),
247
+ });
248
+
249
+ await graph.build('/project/src/a.js');
250
+ await graph.build('/project/src/c.js');
251
+
252
+ const calculator = new CouplingCalculator(graph);
253
+ const hubs = calculator.getHubFiles({ threshold: 3 });
254
+
255
+ expect(hubs).toHaveLength(0);
256
+ });
257
+ });
258
+
259
+ describe('dependent files (high efferent)', () => {
260
+ it('identifies dependent files with high efferent coupling', async () => {
261
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
262
+ const { DependencyGraph } = await import('./dependency-graph.js');
263
+
264
+ const graph = new DependencyGraph({
265
+ basePath: '/project',
266
+ fileExists: () => true,
267
+ readFile: vi.fn().mockImplementation((path) => {
268
+ // index.js imports 5 files (high efferent)
269
+ if (path === '/project/src/index.js') {
270
+ return `
271
+ import './a';
272
+ import './b';
273
+ import './c';
274
+ import './d';
275
+ import './e';
276
+ `;
277
+ }
278
+ // simple.js imports 1 file (low efferent)
279
+ if (path === '/project/src/simple.js') return `import './a';`;
280
+ return '';
281
+ }),
282
+ });
283
+
284
+ await graph.build('/project/src/index.js');
285
+ await graph.build('/project/src/simple.js');
286
+
287
+ const calculator = new CouplingCalculator(graph);
288
+ const dependent = calculator.getDependentFiles({ threshold: 3 });
289
+
290
+ expect(dependent).toHaveLength(1);
291
+ expect(dependent[0].file).toBe('/project/src/index.js');
292
+ expect(dependent[0].efferentCoupling).toBe(5);
293
+ });
294
+
295
+ it('returns empty array when no high efferent files exist', async () => {
296
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
297
+ const { DependencyGraph } = await import('./dependency-graph.js');
298
+
299
+ const graph = new DependencyGraph({
300
+ basePath: '/project',
301
+ fileExists: () => true,
302
+ readFile: vi.fn().mockImplementation((path) => {
303
+ if (path === '/project/src/a.js') return `import './b';`;
304
+ if (path === '/project/src/c.js') return `import './d';`;
305
+ return '';
306
+ }),
307
+ });
308
+
309
+ await graph.build('/project/src/a.js');
310
+ await graph.build('/project/src/c.js');
311
+
312
+ const calculator = new CouplingCalculator(graph);
313
+ const dependent = calculator.getDependentFiles({ threshold: 3 });
314
+
315
+ expect(dependent).toHaveLength(0);
316
+ });
317
+ });
318
+
319
+ describe('isolated files (no coupling)', () => {
320
+ it('handles isolated files with no coupling', async () => {
321
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
322
+ const { DependencyGraph } = await import('./dependency-graph.js');
323
+
324
+ const graph = new DependencyGraph({
325
+ basePath: '/project',
326
+ fileExists: () => true,
327
+ readFile: vi.fn().mockResolvedValue('// no imports'),
328
+ });
329
+
330
+ await graph.build('/project/src/isolated.js');
331
+
332
+ const calculator = new CouplingCalculator(graph);
333
+
334
+ expect(calculator.getAfferentCoupling('/project/src/isolated.js')).toBe(0);
335
+ expect(calculator.getEfferentCoupling('/project/src/isolated.js')).toBe(0);
336
+ expect(calculator.getInstability('/project/src/isolated.js')).toBe(0);
337
+ });
338
+
339
+ it('identifies isolated files', async () => {
340
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
341
+ const { DependencyGraph } = await import('./dependency-graph.js');
342
+
343
+ const graph = new DependencyGraph({
344
+ basePath: '/project',
345
+ fileExists: () => true,
346
+ readFile: vi.fn().mockImplementation((path) => {
347
+ if (path === '/project/src/connected.js') return `import './utils';`;
348
+ return ''; // isolated files have no imports
349
+ }),
350
+ });
351
+
352
+ await graph.build('/project/src/connected.js');
353
+ await graph.build('/project/src/isolated.js');
354
+
355
+ const calculator = new CouplingCalculator(graph);
356
+ const isolated = calculator.getIsolatedFiles();
357
+
358
+ expect(isolated).toContain('/project/src/isolated.js');
359
+ expect(isolated).not.toContain('/project/src/connected.js');
360
+ expect(isolated).not.toContain('/project/src/utils.js');
361
+ });
362
+ });
363
+
364
+ describe('coupling matrix', () => {
365
+ it('generates coupling matrix', async () => {
366
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
367
+ const { DependencyGraph } = await import('./dependency-graph.js');
368
+
369
+ const graph = new DependencyGraph({
370
+ basePath: '/project',
371
+ fileExists: () => true,
372
+ readFile: vi.fn().mockImplementation((path) => {
373
+ if (path === '/project/src/a.js') return `import './b'; import './c';`;
374
+ if (path === '/project/src/b.js') return `import './c';`;
375
+ return '';
376
+ }),
377
+ });
378
+
379
+ await graph.build('/project/src/a.js');
380
+ await graph.build('/project/src/b.js');
381
+
382
+ const calculator = new CouplingCalculator(graph);
383
+ const matrix = calculator.getCouplingMatrix();
384
+
385
+ expect(matrix.files).toHaveLength(3);
386
+ expect(matrix.matrix).toBeDefined();
387
+
388
+ // a.js imports b.js
389
+ const aIndex = matrix.files.indexOf('/project/src/a.js');
390
+ const bIndex = matrix.files.indexOf('/project/src/b.js');
391
+ const cIndex = matrix.files.indexOf('/project/src/c.js');
392
+
393
+ expect(matrix.matrix[aIndex][bIndex]).toBe(1);
394
+ expect(matrix.matrix[aIndex][cIndex]).toBe(1);
395
+ expect(matrix.matrix[bIndex][cIndex]).toBe(1);
396
+ expect(matrix.matrix[bIndex][aIndex]).toBe(0);
397
+ });
398
+
399
+ it('returns empty matrix for empty graph', async () => {
400
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
401
+ const { DependencyGraph } = await import('./dependency-graph.js');
402
+
403
+ const graph = new DependencyGraph({
404
+ basePath: '/project',
405
+ fileExists: () => true,
406
+ readFile: vi.fn().mockResolvedValue(''),
407
+ });
408
+
409
+ const calculator = new CouplingCalculator(graph);
410
+ const matrix = calculator.getCouplingMatrix();
411
+
412
+ expect(matrix.files).toHaveLength(0);
413
+ expect(matrix.matrix).toHaveLength(0);
414
+ });
415
+ });
416
+
417
+ describe('coupling metrics for all files', () => {
418
+ it('calculates coupling metrics for all files', async () => {
419
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
420
+ const { DependencyGraph } = await import('./dependency-graph.js');
421
+
422
+ const graph = new DependencyGraph({
423
+ basePath: '/project',
424
+ fileExists: () => true,
425
+ readFile: vi.fn().mockImplementation((path) => {
426
+ if (path === '/project/src/index.js') return `import './utils';`;
427
+ if (path === '/project/src/utils.js') return `import './config';`;
428
+ return '';
429
+ }),
430
+ });
431
+
432
+ await graph.build('/project/src/index.js');
433
+
434
+ const calculator = new CouplingCalculator(graph);
435
+ const metrics = calculator.getAllMetrics();
436
+
437
+ expect(metrics).toHaveLength(3);
438
+
439
+ const indexMetrics = metrics.find(m => m.file === '/project/src/index.js');
440
+ expect(indexMetrics.afferentCoupling).toBe(0);
441
+ expect(indexMetrics.efferentCoupling).toBe(1);
442
+ expect(indexMetrics.instability).toBe(1);
443
+
444
+ const utilsMetrics = metrics.find(m => m.file === '/project/src/utils.js');
445
+ expect(utilsMetrics.afferentCoupling).toBe(1);
446
+ expect(utilsMetrics.efferentCoupling).toBe(1);
447
+ expect(utilsMetrics.instability).toBe(0.5);
448
+
449
+ const configMetrics = metrics.find(m => m.file === '/project/src/config.js');
450
+ expect(configMetrics.afferentCoupling).toBe(1);
451
+ expect(configMetrics.efferentCoupling).toBe(0);
452
+ expect(configMetrics.instability).toBe(0);
453
+ });
454
+ });
455
+
456
+ describe('highly coupled modules', () => {
457
+ it('identifies highly coupled modules', async () => {
458
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
459
+ const { DependencyGraph } = await import('./dependency-graph.js');
460
+
461
+ const graph = new DependencyGraph({
462
+ basePath: '/project',
463
+ fileExists: () => true,
464
+ readFile: vi.fn().mockImplementation((path) => {
465
+ // index.js is highly coupled (imports 4 files = 4 efferent)
466
+ if (path === '/project/src/index.js') {
467
+ return `
468
+ import './a';
469
+ import './b';
470
+ import './c';
471
+ import './d';
472
+ `;
473
+ }
474
+ return '';
475
+ }),
476
+ });
477
+
478
+ await graph.build('/project/src/index.js');
479
+
480
+ const calculator = new CouplingCalculator(graph);
481
+ const highlyCoupled = calculator.getHighlyCoupledModules({ threshold: 4 });
482
+
483
+ expect(highlyCoupled).toHaveLength(1);
484
+ expect(highlyCoupled[0].file).toBe('/project/src/index.js');
485
+ expect(highlyCoupled[0].totalCoupling).toBe(4);
486
+ });
487
+
488
+ it('returns empty array when no highly coupled modules exist', async () => {
489
+ const { CouplingCalculator } = await import('./coupling-calculator.js');
490
+ const { DependencyGraph } = await import('./dependency-graph.js');
491
+
492
+ const graph = new DependencyGraph({
493
+ basePath: '/project',
494
+ fileExists: () => true,
495
+ readFile: vi.fn().mockImplementation((path) => {
496
+ if (path === '/project/src/a.js') return `import './b';`;
497
+ return '';
498
+ }),
499
+ });
500
+
501
+ await graph.build('/project/src/a.js');
502
+
503
+ const calculator = new CouplingCalculator(graph);
504
+ const highlyCoupled = calculator.getHighlyCoupledModules({ threshold: 4 });
505
+
506
+ expect(highlyCoupled).toHaveLength(0);
507
+ });
508
+ });
509
+ });