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,447 @@
1
+ /**
2
+ * Cohesion Analyzer Tests
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ // Mock DependencyGraph for testing
8
+ function createMockGraph(nodes, edges) {
9
+ const graphData = {
10
+ nodes: nodes.map(n => ({
11
+ id: n.id,
12
+ name: n.name,
13
+ imports: 0,
14
+ importedBy: 0,
15
+ })),
16
+ edges: edges.map(e => ({
17
+ from: e.from,
18
+ to: e.to,
19
+ fromName: nodes.find(n => n.id === e.from)?.name || e.from,
20
+ toName: nodes.find(n => n.id === e.to)?.name || e.to,
21
+ })),
22
+ external: [],
23
+ stats: {
24
+ totalFiles: nodes.length,
25
+ totalEdges: edges.length,
26
+ externalDeps: 0,
27
+ },
28
+ };
29
+
30
+ // Build imports/importedBy maps
31
+ const importsMap = {};
32
+ const importersMap = {};
33
+
34
+ for (const node of nodes) {
35
+ importsMap[node.id] = [];
36
+ importersMap[node.id] = [];
37
+ }
38
+
39
+ for (const edge of edges) {
40
+ if (importsMap[edge.from]) {
41
+ importsMap[edge.from].push(edge.to);
42
+ }
43
+ if (importersMap[edge.to]) {
44
+ importersMap[edge.to].push(edge.from);
45
+ }
46
+ }
47
+
48
+ return {
49
+ getGraph: () => graphData,
50
+ getImports: (file) => importsMap[file] || [],
51
+ getImporters: (file) => importersMap[file] || [],
52
+ };
53
+ }
54
+
55
+ describe('CohesionAnalyzer', () => {
56
+ describe('groupByDirectory', () => {
57
+ it('groups files by directory correctly', async () => {
58
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
59
+
60
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
61
+ const nodes = [
62
+ { id: '/project/src/utils/helpers.js', name: 'src/utils/helpers.js' },
63
+ { id: '/project/src/utils/format.js', name: 'src/utils/format.js' },
64
+ { id: '/project/src/components/Button.js', name: 'src/components/Button.js' },
65
+ { id: '/project/src/index.js', name: 'src/index.js' },
66
+ ];
67
+
68
+ const modules = analyzer.groupByDirectory(nodes);
69
+
70
+ expect(Object.keys(modules)).toHaveLength(3);
71
+ expect(modules['src/utils']).toHaveLength(2);
72
+ expect(modules['src/components']).toHaveLength(1);
73
+ expect(modules['src']).toHaveLength(1);
74
+ });
75
+
76
+ it('puts root files in (root) module', async () => {
77
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
78
+
79
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
80
+ const nodes = [
81
+ { id: '/project/index.js', name: 'index.js' },
82
+ { id: '/project/config.js', name: 'config.js' },
83
+ ];
84
+
85
+ const modules = analyzer.groupByDirectory(nodes);
86
+
87
+ expect(modules['(root)']).toHaveLength(2);
88
+ });
89
+ });
90
+
91
+ describe('high cohesion', () => {
92
+ it('scores high when files only import each other', async () => {
93
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
94
+
95
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
96
+
97
+ // Module where files only import each other
98
+ const nodes = [
99
+ { id: '/project/src/utils/a.js', name: 'src/utils/a.js' },
100
+ { id: '/project/src/utils/b.js', name: 'src/utils/b.js' },
101
+ { id: '/project/src/utils/c.js', name: 'src/utils/c.js' },
102
+ ];
103
+
104
+ const edges = [
105
+ { from: '/project/src/utils/a.js', to: '/project/src/utils/b.js' },
106
+ { from: '/project/src/utils/b.js', to: '/project/src/utils/c.js' },
107
+ { from: '/project/src/utils/a.js', to: '/project/src/utils/c.js' },
108
+ ];
109
+
110
+ const mockGraph = createMockGraph(nodes, edges);
111
+ const result = analyzer.analyze(mockGraph);
112
+
113
+ expect(result.modules['src/utils'].cohesion).toBe(1);
114
+ expect(result.lowCohesion).toHaveLength(0);
115
+ });
116
+
117
+ it('reports high average cohesion for well-structured codebase', async () => {
118
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
119
+
120
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
121
+
122
+ const nodes = [
123
+ { id: '/project/src/utils/a.js', name: 'src/utils/a.js' },
124
+ { id: '/project/src/utils/b.js', name: 'src/utils/b.js' },
125
+ { id: '/project/src/lib/x.js', name: 'src/lib/x.js' },
126
+ { id: '/project/src/lib/y.js', name: 'src/lib/y.js' },
127
+ ];
128
+
129
+ const edges = [
130
+ { from: '/project/src/utils/a.js', to: '/project/src/utils/b.js' },
131
+ { from: '/project/src/lib/x.js', to: '/project/src/lib/y.js' },
132
+ ];
133
+
134
+ const mockGraph = createMockGraph(nodes, edges);
135
+ const result = analyzer.analyze(mockGraph);
136
+
137
+ expect(result.summary.averageCohesion).toBe(1);
138
+ });
139
+ });
140
+
141
+ describe('low cohesion', () => {
142
+ it('scores low when files mostly import external', async () => {
143
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
144
+
145
+ const analyzer = new CohesionAnalyzer({
146
+ basePath: '/project',
147
+ lowCohesionThreshold: 0.3,
148
+ });
149
+
150
+ // Module where files import mostly from other modules
151
+ const nodes = [
152
+ { id: '/project/src/utils/a.js', name: 'src/utils/a.js' },
153
+ { id: '/project/src/utils/b.js', name: 'src/utils/b.js' },
154
+ { id: '/project/src/lib/x.js', name: 'src/lib/x.js' },
155
+ { id: '/project/src/lib/y.js', name: 'src/lib/y.js' },
156
+ { id: '/project/src/lib/z.js', name: 'src/lib/z.js' },
157
+ ];
158
+
159
+ // a and b import from lib instead of each other
160
+ const edges = [
161
+ { from: '/project/src/utils/a.js', to: '/project/src/lib/x.js' },
162
+ { from: '/project/src/utils/a.js', to: '/project/src/lib/y.js' },
163
+ { from: '/project/src/utils/b.js', to: '/project/src/lib/x.js' },
164
+ { from: '/project/src/utils/b.js', to: '/project/src/lib/z.js' },
165
+ ];
166
+
167
+ const mockGraph = createMockGraph(nodes, edges);
168
+ const result = analyzer.analyze(mockGraph);
169
+
170
+ expect(result.modules['src/utils'].cohesion).toBe(0);
171
+ expect(result.lowCohesion.length).toBeGreaterThan(0);
172
+ expect(result.lowCohesion[0].module).toBe('src/utils');
173
+ });
174
+
175
+ it('identifies all low cohesion modules', async () => {
176
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
177
+
178
+ const analyzer = new CohesionAnalyzer({
179
+ basePath: '/project',
180
+ lowCohesionThreshold: 0.5,
181
+ });
182
+
183
+ const nodes = [
184
+ { id: '/project/src/a/1.js', name: 'src/a/1.js' },
185
+ { id: '/project/src/a/2.js', name: 'src/a/2.js' },
186
+ { id: '/project/src/b/1.js', name: 'src/b/1.js' },
187
+ { id: '/project/src/b/2.js', name: 'src/b/2.js' },
188
+ { id: '/project/src/shared.js', name: 'src/shared.js' },
189
+ ];
190
+
191
+ // Both modules depend heavily on shared
192
+ const edges = [
193
+ { from: '/project/src/a/1.js', to: '/project/src/shared.js' },
194
+ { from: '/project/src/a/2.js', to: '/project/src/shared.js' },
195
+ { from: '/project/src/b/1.js', to: '/project/src/shared.js' },
196
+ { from: '/project/src/b/2.js', to: '/project/src/shared.js' },
197
+ ];
198
+
199
+ const mockGraph = createMockGraph(nodes, edges);
200
+ const result = analyzer.analyze(mockGraph);
201
+
202
+ // Both src/a and src/b should be low cohesion
203
+ const lowModules = result.lowCohesion.map(l => l.module);
204
+ expect(lowModules).toContain('src/a');
205
+ expect(lowModules).toContain('src/b');
206
+ });
207
+ });
208
+
209
+ describe('suggestions', () => {
210
+ it('suggests moving outlier files for better cohesion', async () => {
211
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
212
+
213
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
214
+
215
+ // File in utils that mostly depends on lib
216
+ const nodes = [
217
+ { id: '/project/src/utils/a.js', name: 'src/utils/a.js' },
218
+ { id: '/project/src/utils/b.js', name: 'src/utils/b.js' },
219
+ { id: '/project/src/utils/outlier.js', name: 'src/utils/outlier.js' },
220
+ { id: '/project/src/lib/x.js', name: 'src/lib/x.js' },
221
+ { id: '/project/src/lib/y.js', name: 'src/lib/y.js' },
222
+ { id: '/project/src/lib/z.js', name: 'src/lib/z.js' },
223
+ ];
224
+
225
+ // outlier imports from lib, not from utils
226
+ const edges = [
227
+ { from: '/project/src/utils/a.js', to: '/project/src/utils/b.js' },
228
+ { from: '/project/src/utils/outlier.js', to: '/project/src/lib/x.js' },
229
+ { from: '/project/src/utils/outlier.js', to: '/project/src/lib/y.js' },
230
+ { from: '/project/src/utils/outlier.js', to: '/project/src/lib/z.js' },
231
+ { from: '/project/src/lib/x.js', to: '/project/src/lib/y.js' },
232
+ ];
233
+
234
+ const mockGraph = createMockGraph(nodes, edges);
235
+ const result = analyzer.analyze(mockGraph);
236
+
237
+ const moveSuggestions = result.suggestions.filter(s => s.type === 'move');
238
+ const outlierSuggestion = moveSuggestions.find(s =>
239
+ s.file.includes('outlier')
240
+ );
241
+
242
+ expect(outlierSuggestion).toBeDefined();
243
+ expect(outlierSuggestion.from).toBe('src/utils');
244
+ expect(outlierSuggestion.to).toBe('src/lib');
245
+ });
246
+
247
+ it('does not suggest moves for well-placed files', async () => {
248
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
249
+
250
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
251
+
252
+ // All files are well-placed
253
+ const nodes = [
254
+ { id: '/project/src/utils/a.js', name: 'src/utils/a.js' },
255
+ { id: '/project/src/utils/b.js', name: 'src/utils/b.js' },
256
+ { id: '/project/src/utils/c.js', name: 'src/utils/c.js' },
257
+ ];
258
+
259
+ const edges = [
260
+ { from: '/project/src/utils/a.js', to: '/project/src/utils/b.js' },
261
+ { from: '/project/src/utils/b.js', to: '/project/src/utils/c.js' },
262
+ { from: '/project/src/utils/a.js', to: '/project/src/utils/c.js' },
263
+ ];
264
+
265
+ const mockGraph = createMockGraph(nodes, edges);
266
+ const result = analyzer.analyze(mockGraph);
267
+
268
+ expect(result.suggestions).toHaveLength(0);
269
+ });
270
+ });
271
+
272
+ describe('single-file modules', () => {
273
+ it('handles single-file modules correctly', async () => {
274
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
275
+
276
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
277
+
278
+ const nodes = [
279
+ { id: '/project/src/utils/single.js', name: 'src/utils/single.js' },
280
+ { id: '/project/src/lib/a.js', name: 'src/lib/a.js' },
281
+ { id: '/project/src/lib/b.js', name: 'src/lib/b.js' },
282
+ ];
283
+
284
+ const edges = [
285
+ { from: '/project/src/utils/single.js', to: '/project/src/lib/a.js' },
286
+ { from: '/project/src/lib/a.js', to: '/project/src/lib/b.js' },
287
+ ];
288
+
289
+ const mockGraph = createMockGraph(nodes, edges);
290
+ const result = analyzer.analyze(mockGraph);
291
+
292
+ // Single file module should have cohesion of 1
293
+ expect(result.modules['src/utils'].cohesion).toBe(1);
294
+ expect(result.modules['src/utils'].singleFile).toBe(true);
295
+ });
296
+
297
+ it('does not mark single-file modules as low cohesion', async () => {
298
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
299
+
300
+ const analyzer = new CohesionAnalyzer({
301
+ basePath: '/project',
302
+ lowCohesionThreshold: 0.5,
303
+ });
304
+
305
+ const nodes = [
306
+ { id: '/project/src/single/only.js', name: 'src/single/only.js' },
307
+ { id: '/project/src/other/a.js', name: 'src/other/a.js' },
308
+ ];
309
+
310
+ const edges = [
311
+ { from: '/project/src/single/only.js', to: '/project/src/other/a.js' },
312
+ ];
313
+
314
+ const mockGraph = createMockGraph(nodes, edges);
315
+ const result = analyzer.analyze(mockGraph);
316
+
317
+ // Single file should not appear in low cohesion list
318
+ const singleInLow = result.lowCohesion.find(
319
+ l => l.module === 'src/single'
320
+ );
321
+ expect(singleInLow).toBeUndefined();
322
+ });
323
+ });
324
+
325
+ describe('empty modules', () => {
326
+ it('handles empty graph', async () => {
327
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
328
+
329
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
330
+ const mockGraph = createMockGraph([], []);
331
+
332
+ const result = analyzer.analyze(mockGraph);
333
+
334
+ expect(result.modules).toEqual({});
335
+ expect(result.lowCohesion).toHaveLength(0);
336
+ expect(result.suggestions).toHaveLength(0);
337
+ expect(result.summary.totalModules).toBe(0);
338
+ expect(result.summary.averageCohesion).toBe(1);
339
+ });
340
+ });
341
+
342
+ describe('summary statistics', () => {
343
+ it('calculates average cohesion correctly', async () => {
344
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
345
+
346
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
347
+
348
+ const nodes = [
349
+ { id: '/project/src/a/1.js', name: 'src/a/1.js' },
350
+ { id: '/project/src/a/2.js', name: 'src/a/2.js' },
351
+ { id: '/project/src/b/1.js', name: 'src/b/1.js' },
352
+ { id: '/project/src/b/2.js', name: 'src/b/2.js' },
353
+ ];
354
+
355
+ // Module a has 100% cohesion, module b has 0%
356
+ const edges = [
357
+ { from: '/project/src/a/1.js', to: '/project/src/a/2.js' },
358
+ { from: '/project/src/b/1.js', to: '/project/src/a/1.js' },
359
+ { from: '/project/src/b/2.js', to: '/project/src/a/2.js' },
360
+ ];
361
+
362
+ const mockGraph = createMockGraph(nodes, edges);
363
+ const result = analyzer.analyze(mockGraph);
364
+
365
+ // Average should be between 0 and 1
366
+ expect(result.summary.averageCohesion).toBeGreaterThanOrEqual(0);
367
+ expect(result.summary.averageCohesion).toBeLessThanOrEqual(1);
368
+ });
369
+
370
+ it('counts total modules correctly', async () => {
371
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
372
+
373
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
374
+
375
+ const nodes = [
376
+ { id: '/project/src/a/1.js', name: 'src/a/1.js' },
377
+ { id: '/project/src/b/1.js', name: 'src/b/1.js' },
378
+ { id: '/project/src/c/1.js', name: 'src/c/1.js' },
379
+ ];
380
+
381
+ const mockGraph = createMockGraph(nodes, []);
382
+ const result = analyzer.analyze(mockGraph);
383
+
384
+ expect(result.summary.totalModules).toBe(3);
385
+ });
386
+
387
+ it('counts low cohesion modules correctly', async () => {
388
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
389
+
390
+ const analyzer = new CohesionAnalyzer({
391
+ basePath: '/project',
392
+ lowCohesionThreshold: 0.5,
393
+ });
394
+
395
+ const nodes = [
396
+ { id: '/project/src/a/1.js', name: 'src/a/1.js' },
397
+ { id: '/project/src/a/2.js', name: 'src/a/2.js' },
398
+ { id: '/project/src/b/1.js', name: 'src/b/1.js' },
399
+ { id: '/project/src/b/2.js', name: 'src/b/2.js' },
400
+ { id: '/project/src/shared.js', name: 'src/shared.js' },
401
+ ];
402
+
403
+ // Both a and b have low cohesion
404
+ const edges = [
405
+ { from: '/project/src/a/1.js', to: '/project/src/shared.js' },
406
+ { from: '/project/src/a/2.js', to: '/project/src/shared.js' },
407
+ { from: '/project/src/b/1.js', to: '/project/src/shared.js' },
408
+ { from: '/project/src/b/2.js', to: '/project/src/shared.js' },
409
+ ];
410
+
411
+ const mockGraph = createMockGraph(nodes, edges);
412
+ const result = analyzer.analyze(mockGraph);
413
+
414
+ expect(result.summary.lowCohesionCount).toBe(2);
415
+ });
416
+ });
417
+
418
+ describe('mixed cohesion scenarios', () => {
419
+ it('handles modules with mixed internal/external deps', async () => {
420
+ const { CohesionAnalyzer } = await import('./cohesion-analyzer.js');
421
+
422
+ const analyzer = new CohesionAnalyzer({ basePath: '/project' });
423
+
424
+ const nodes = [
425
+ { id: '/project/src/utils/a.js', name: 'src/utils/a.js' },
426
+ { id: '/project/src/utils/b.js', name: 'src/utils/b.js' },
427
+ { id: '/project/src/utils/c.js', name: 'src/utils/c.js' },
428
+ { id: '/project/src/lib/x.js', name: 'src/lib/x.js' },
429
+ ];
430
+
431
+ // Some internal deps, some external
432
+ const edges = [
433
+ { from: '/project/src/utils/a.js', to: '/project/src/utils/b.js' },
434
+ { from: '/project/src/utils/b.js', to: '/project/src/utils/c.js' },
435
+ { from: '/project/src/utils/a.js', to: '/project/src/lib/x.js' },
436
+ ];
437
+
438
+ const mockGraph = createMockGraph(nodes, edges);
439
+ const result = analyzer.analyze(mockGraph);
440
+
441
+ // Cohesion should be between 0 and 1
442
+ const utilsCohesion = result.modules['src/utils'].cohesion;
443
+ expect(utilsCohesion).toBeGreaterThan(0);
444
+ expect(utilsCohesion).toBeLessThan(1);
445
+ });
446
+ });
447
+ });