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.
- package/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/UsagePane.d.ts +13 -0
- package/dashboard/dist/components/UsagePane.js +51 -0
- package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
- package/dashboard/dist/components/UsagePane.test.js +142 -0
- package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
- package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
- package/dashboard/dist/components/WorkspacePane.js +17 -0
- package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspacePane.test.js +84 -0
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/architecture-command.js +450 -0
- package/server/lib/architecture-command.test.js +754 -0
- package/server/lib/ast-analyzer.js +324 -0
- package/server/lib/ast-analyzer.test.js +437 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/auth-system.test.js +4 -1
- package/server/lib/boundary-detector.js +427 -0
- package/server/lib/boundary-detector.test.js +320 -0
- package/server/lib/budget-alerts.js +138 -0
- package/server/lib/budget-alerts.test.js +235 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/candidates-tracker.js +210 -0
- package/server/lib/candidates-tracker.test.js +300 -0
- package/server/lib/checkpoint-manager.js +251 -0
- package/server/lib/checkpoint-manager.test.js +474 -0
- package/server/lib/circular-detector.js +337 -0
- package/server/lib/circular-detector.test.js +353 -0
- package/server/lib/cohesion-analyzer.js +310 -0
- package/server/lib/cohesion-analyzer.test.js +447 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/contract-testing.js +625 -0
- package/server/lib/contract-testing.test.js +342 -0
- package/server/lib/conversion-planner.js +469 -0
- package/server/lib/conversion-planner.test.js +361 -0
- package/server/lib/convert-command.js +351 -0
- package/server/lib/convert-command.test.js +608 -0
- package/server/lib/coupling-calculator.js +189 -0
- package/server/lib/coupling-calculator.test.js +509 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/dependency-graph.js +367 -0
- package/server/lib/dependency-graph.test.js +516 -0
- package/server/lib/duplication-detector.js +349 -0
- package/server/lib/duplication-detector.test.js +401 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mermaid-generator.js +358 -0
- package/server/lib/mermaid-generator.test.js +301 -0
- package/server/lib/messaging-patterns.js +750 -0
- package/server/lib/messaging-patterns.test.js +213 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/microservice-template.js +386 -0
- package/server/lib/microservice-template.test.js +325 -0
- package/server/lib/new-project-microservice.js +450 -0
- package/server/lib/new-project-microservice.test.js +600 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/refactor-command.js +326 -0
- package/server/lib/refactor-command.test.js +528 -0
- package/server/lib/refactor-executor.js +254 -0
- package/server/lib/refactor-executor.test.js +305 -0
- package/server/lib/refactor-observer.js +292 -0
- package/server/lib/refactor-observer.test.js +422 -0
- package/server/lib/refactor-progress.js +193 -0
- package/server/lib/refactor-progress.test.js +251 -0
- package/server/lib/refactor-reporter.js +237 -0
- package/server/lib/refactor-reporter.test.js +247 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/traefik-config.js +282 -0
- package/server/lib/traefik-config.test.js +312 -0
- package/server/lib/usage-command.js +218 -0
- package/server/lib/usage-command.test.js +391 -0
- package/server/lib/usage-formatter.js +192 -0
- package/server/lib/usage-formatter.test.js +267 -0
- package/server/lib/usage-history.js +122 -0
- package/server/lib/usage-history.test.js +206 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
- package/server/package-lock.json +14 -0
- 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
|
+
});
|