tlc-claude-code 1.2.28 → 1.3.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/README.md +9 -4
- 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 +146 -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/package.json +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -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/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/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/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/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/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -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/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/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/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -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/package-lock.json +14 -0
- package/server/package.json +1 -0
- package/templates/docs-sync.yml +91 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact Scorer Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
describe('ImpactScorer', () => {
|
|
8
|
+
describe('complexity reduction scoring', () => {
|
|
9
|
+
it('high complexity reduction gives high score', async () => {
|
|
10
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
11
|
+
const scorer = new ImpactScorer();
|
|
12
|
+
|
|
13
|
+
const result = scorer.score({
|
|
14
|
+
complexity: 25,
|
|
15
|
+
targetComplexity: 5,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(result.breakdown.complexityReduction).toBeGreaterThan(70);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('low complexity reduction gives low score', async () => {
|
|
22
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
23
|
+
const scorer = new ImpactScorer();
|
|
24
|
+
|
|
25
|
+
const result = scorer.score({
|
|
26
|
+
complexity: 3,
|
|
27
|
+
targetComplexity: 2,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.breakdown.complexityReduction).toBeLessThan(50);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('blast radius scoring', () => {
|
|
35
|
+
it('many files affected gives higher score', async () => {
|
|
36
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
37
|
+
const scorer = new ImpactScorer();
|
|
38
|
+
|
|
39
|
+
const result = scorer.score({
|
|
40
|
+
filesAffected: 15,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.breakdown.blastRadius).toBeGreaterThan(80);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('single file gives lower score', async () => {
|
|
47
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
48
|
+
const scorer = new ImpactScorer();
|
|
49
|
+
|
|
50
|
+
const result = scorer.score({
|
|
51
|
+
filesAffected: 1,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.breakdown.blastRadius).toBeLessThan(60);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('change frequency scoring', () => {
|
|
59
|
+
it('frequently changed files get higher score', async () => {
|
|
60
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
61
|
+
const scorer = new ImpactScorer();
|
|
62
|
+
|
|
63
|
+
const result = scorer.score({
|
|
64
|
+
changeCount: 100,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.breakdown.changeFrequency).toBeGreaterThan(80);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('rarely changed files get lower score', async () => {
|
|
71
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
72
|
+
const scorer = new ImpactScorer();
|
|
73
|
+
|
|
74
|
+
const result = scorer.score({
|
|
75
|
+
changeCount: 2,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.breakdown.changeFrequency).toBeLessThan(60);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('risk scoring', () => {
|
|
83
|
+
it('low test coverage gives higher risk score', async () => {
|
|
84
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
85
|
+
const scorer = new ImpactScorer();
|
|
86
|
+
|
|
87
|
+
const result = scorer.score({
|
|
88
|
+
testCoverage: 10,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.breakdown.risk).toBeGreaterThan(80);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('high test coverage gives lower risk score', async () => {
|
|
95
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
96
|
+
const scorer = new ImpactScorer();
|
|
97
|
+
|
|
98
|
+
const result = scorer.score({
|
|
99
|
+
testCoverage: 90,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.breakdown.risk).toBeLessThan(50);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('critical paths get boosted score', async () => {
|
|
106
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
107
|
+
const scorer = new ImpactScorer();
|
|
108
|
+
|
|
109
|
+
const regular = scorer.score({ testCoverage: 50 });
|
|
110
|
+
const critical = scorer.score({ testCoverage: 50, isCritical: true });
|
|
111
|
+
|
|
112
|
+
expect(critical.breakdown.risk).toBeGreaterThan(regular.breakdown.risk);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('combined scoring', () => {
|
|
117
|
+
it('combines factors into single 0-100 score', async () => {
|
|
118
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
119
|
+
const scorer = new ImpactScorer();
|
|
120
|
+
|
|
121
|
+
const result = scorer.score({
|
|
122
|
+
complexity: 20,
|
|
123
|
+
filesAffected: 5,
|
|
124
|
+
changeCount: 30,
|
|
125
|
+
testCoverage: 40,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result.total).toBeGreaterThanOrEqual(0);
|
|
129
|
+
expect(result.total).toBeLessThanOrEqual(100);
|
|
130
|
+
expect(typeof result.total).toBe('number');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns breakdown of individual scores', async () => {
|
|
134
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
135
|
+
const scorer = new ImpactScorer();
|
|
136
|
+
|
|
137
|
+
const result = scorer.score({
|
|
138
|
+
complexity: 10,
|
|
139
|
+
filesAffected: 3,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.breakdown).toBeDefined();
|
|
143
|
+
expect(result.breakdown.complexityReduction).toBeDefined();
|
|
144
|
+
expect(result.breakdown.blastRadius).toBeDefined();
|
|
145
|
+
expect(result.breakdown.changeFrequency).toBeDefined();
|
|
146
|
+
expect(result.breakdown.risk).toBeDefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('git history', () => {
|
|
151
|
+
it('handles missing git history gracefully', async () => {
|
|
152
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
153
|
+
|
|
154
|
+
const execMock = vi.fn().mockReturnValue('');
|
|
155
|
+
const scorer = new ImpactScorer({ exec: execMock });
|
|
156
|
+
|
|
157
|
+
const result = scorer.score({
|
|
158
|
+
filePath: 'nonexistent.js',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result.total).toBeGreaterThanOrEqual(0);
|
|
162
|
+
// Empty git output returns 0 commits = score 30
|
|
163
|
+
expect(result.breakdown.changeFrequency).toBe(30);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('uses git log for file history when available', async () => {
|
|
167
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
168
|
+
|
|
169
|
+
const execMock = vi.fn().mockReturnValue('25\n');
|
|
170
|
+
const scorer = new ImpactScorer({ exec: execMock });
|
|
171
|
+
|
|
172
|
+
const result = scorer.score({
|
|
173
|
+
filePath: 'src/api/users.js',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(execMock).toHaveBeenCalledWith(expect.stringContaining('git log'));
|
|
177
|
+
expect(result.breakdown.changeFrequency).toBeGreaterThan(60);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('scoreAll', () => {
|
|
182
|
+
it('scores and sorts multiple opportunities', async () => {
|
|
183
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
184
|
+
const scorer = new ImpactScorer();
|
|
185
|
+
|
|
186
|
+
const opportunities = [
|
|
187
|
+
{ complexity: 5, filesAffected: 1 },
|
|
188
|
+
{ complexity: 30, filesAffected: 10, changeCount: 50 },
|
|
189
|
+
{ complexity: 10, filesAffected: 3 },
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const scored = scorer.scoreAll(opportunities);
|
|
193
|
+
|
|
194
|
+
expect(scored).toHaveLength(3);
|
|
195
|
+
expect(scored[0].impact.total).toBeGreaterThanOrEqual(scored[1].impact.total);
|
|
196
|
+
expect(scored[1].impact.total).toBeGreaterThanOrEqual(scored[2].impact.total);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('getTier', () => {
|
|
201
|
+
it('returns correct priority tier', async () => {
|
|
202
|
+
const { ImpactScorer } = await import('./impact-scorer.js');
|
|
203
|
+
|
|
204
|
+
expect(ImpactScorer.getTier(90)).toBe('high');
|
|
205
|
+
expect(ImpactScorer.getTier(80)).toBe('high');
|
|
206
|
+
expect(ImpactScorer.getTier(65)).toBe('medium');
|
|
207
|
+
expect(ImpactScorer.getTier(50)).toBe('medium');
|
|
208
|
+
expect(ImpactScorer.getTier(30)).toBe('low');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid Diagram Generator
|
|
3
|
+
* Generate Mermaid diagrams from dependency data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
class MermaidGenerator {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = {
|
|
11
|
+
direction: options.direction || 'TD', // TB, BT, LR, RL
|
|
12
|
+
maxNodes: options.maxNodes || 100,
|
|
13
|
+
showExternal: options.showExternal !== false,
|
|
14
|
+
groupByDirectory: options.groupByDirectory !== false,
|
|
15
|
+
highlightCycles: options.highlightCycles !== false,
|
|
16
|
+
highlightHubs: options.highlightHubs || false,
|
|
17
|
+
hubThreshold: options.hubThreshold || 5,
|
|
18
|
+
...options,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate flowchart from dependency graph
|
|
24
|
+
* @param {Object} graph - Graph from DependencyGraph.getGraph()
|
|
25
|
+
* @param {Object} options - Generation options
|
|
26
|
+
* @returns {string} Mermaid diagram code
|
|
27
|
+
*/
|
|
28
|
+
generateFlowchart(graph, options = {}) {
|
|
29
|
+
const opts = { ...this.options, ...options };
|
|
30
|
+
const { nodes, edges, external } = graph;
|
|
31
|
+
|
|
32
|
+
// Truncate if too large
|
|
33
|
+
const displayNodes = nodes.slice(0, opts.maxNodes);
|
|
34
|
+
const nodeIds = new Set(displayNodes.map(n => n.id));
|
|
35
|
+
const displayEdges = edges.filter(e => nodeIds.has(e.from) && nodeIds.has(e.to));
|
|
36
|
+
|
|
37
|
+
let mermaid = `flowchart ${opts.direction}\n`;
|
|
38
|
+
|
|
39
|
+
// Add styling
|
|
40
|
+
mermaid += this.generateStyles(opts);
|
|
41
|
+
|
|
42
|
+
// Group by directory if enabled
|
|
43
|
+
if (opts.groupByDirectory) {
|
|
44
|
+
mermaid += this.generateSubgraphs(displayNodes, displayEdges, opts);
|
|
45
|
+
} else {
|
|
46
|
+
mermaid += this.generateFlatNodes(displayNodes, opts);
|
|
47
|
+
mermaid += this.generateEdges(displayEdges, opts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Highlight cycles if provided
|
|
51
|
+
if (opts.cycles && opts.cycles.length > 0 && opts.highlightCycles) {
|
|
52
|
+
mermaid += this.generateCycleHighlights(opts.cycles);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Show external deps if enabled
|
|
56
|
+
if (opts.showExternal && external && external.length > 0) {
|
|
57
|
+
mermaid += this.generateExternalSubgraph(external.slice(0, 20));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Truncation notice
|
|
61
|
+
if (nodes.length > opts.maxNodes) {
|
|
62
|
+
mermaid += `\n %% Showing ${opts.maxNodes} of ${nodes.length} files\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return mermaid;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate styles section
|
|
70
|
+
*/
|
|
71
|
+
generateStyles(opts) {
|
|
72
|
+
let styles = '';
|
|
73
|
+
|
|
74
|
+
if (opts.highlightCycles) {
|
|
75
|
+
styles += ' classDef cycle fill:#f96,stroke:#f00,stroke-width:2px\n';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (opts.highlightHubs) {
|
|
79
|
+
styles += ' classDef hub fill:#9f6,stroke:#090,stroke-width:2px\n';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
styles += ' classDef external fill:#ddd,stroke:#999\n';
|
|
83
|
+
|
|
84
|
+
return styles;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate subgraphs for directories
|
|
89
|
+
*/
|
|
90
|
+
generateSubgraphs(nodes, edges, opts) {
|
|
91
|
+
let mermaid = '';
|
|
92
|
+
const directories = this.groupByDirectory(nodes);
|
|
93
|
+
const nodeIdMap = new Map();
|
|
94
|
+
|
|
95
|
+
// Generate subgraphs
|
|
96
|
+
for (const [dir, dirNodes] of directories.entries()) {
|
|
97
|
+
const safeDirId = this.sanitizeId(dir || 'root');
|
|
98
|
+
const dirLabel = dir || 'Root';
|
|
99
|
+
|
|
100
|
+
mermaid += ` subgraph ${safeDirId}[${this.escapeLabel(dirLabel)}]\n`;
|
|
101
|
+
|
|
102
|
+
for (const node of dirNodes) {
|
|
103
|
+
const nodeId = this.sanitizeId(node.name);
|
|
104
|
+
nodeIdMap.set(node.id, nodeId);
|
|
105
|
+
const label = path.basename(node.name);
|
|
106
|
+
mermaid += ` ${nodeId}[${this.escapeLabel(label)}]\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
mermaid += ' end\n';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Generate edges
|
|
113
|
+
for (const edge of edges) {
|
|
114
|
+
const fromId = nodeIdMap.get(edge.from);
|
|
115
|
+
const toId = nodeIdMap.get(edge.to);
|
|
116
|
+
if (fromId && toId) {
|
|
117
|
+
mermaid += ` ${fromId} --> ${toId}\n`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return mermaid;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generate flat node list (no subgraphs)
|
|
126
|
+
*/
|
|
127
|
+
generateFlatNodes(nodes, opts) {
|
|
128
|
+
let mermaid = '';
|
|
129
|
+
|
|
130
|
+
for (const node of nodes) {
|
|
131
|
+
const nodeId = this.sanitizeId(node.name);
|
|
132
|
+
const label = node.name;
|
|
133
|
+
mermaid += ` ${nodeId}[${this.escapeLabel(label)}]\n`;
|
|
134
|
+
|
|
135
|
+
// Mark hubs
|
|
136
|
+
if (opts.highlightHubs && node.importedBy >= opts.hubThreshold) {
|
|
137
|
+
mermaid += ` class ${nodeId} hub\n`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return mermaid;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate edges
|
|
146
|
+
*/
|
|
147
|
+
generateEdges(edges, opts) {
|
|
148
|
+
let mermaid = '';
|
|
149
|
+
|
|
150
|
+
for (const edge of edges) {
|
|
151
|
+
const fromId = this.sanitizeId(edge.fromName);
|
|
152
|
+
const toId = this.sanitizeId(edge.toName);
|
|
153
|
+
mermaid += ` ${fromId} --> ${toId}\n`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return mermaid;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Highlight cycle nodes
|
|
161
|
+
*/
|
|
162
|
+
generateCycleHighlights(cycles) {
|
|
163
|
+
let mermaid = '\n %% Circular dependencies\n';
|
|
164
|
+
const cycleNodes = new Set();
|
|
165
|
+
|
|
166
|
+
for (const cycle of cycles) {
|
|
167
|
+
for (const node of cycle.path || cycle) {
|
|
168
|
+
cycleNodes.add(this.sanitizeId(node));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const nodeId of cycleNodes) {
|
|
173
|
+
mermaid += ` class ${nodeId} cycle\n`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return mermaid;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Generate external dependencies subgraph
|
|
181
|
+
*/
|
|
182
|
+
generateExternalSubgraph(external) {
|
|
183
|
+
let mermaid = '\n subgraph external[External Dependencies]\n';
|
|
184
|
+
|
|
185
|
+
for (const dep of external) {
|
|
186
|
+
const nodeId = this.sanitizeId(`ext_${dep}`);
|
|
187
|
+
mermaid += ` ${nodeId}[${this.escapeLabel(dep)}]:::external\n`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
mermaid += ' end\n';
|
|
191
|
+
return mermaid;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Group nodes by directory
|
|
196
|
+
*/
|
|
197
|
+
groupByDirectory(nodes) {
|
|
198
|
+
const directories = new Map();
|
|
199
|
+
|
|
200
|
+
for (const node of nodes) {
|
|
201
|
+
const dir = path.dirname(node.name);
|
|
202
|
+
if (!directories.has(dir)) {
|
|
203
|
+
directories.set(dir, []);
|
|
204
|
+
}
|
|
205
|
+
directories.get(dir).push(node);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return directories;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generate module-filtered diagram
|
|
213
|
+
* @param {Object} graph - Full graph
|
|
214
|
+
* @param {string} modulePath - Module path to filter to
|
|
215
|
+
* @returns {string} Mermaid diagram
|
|
216
|
+
*/
|
|
217
|
+
generateModuleDiagram(graph, modulePath, options = {}) {
|
|
218
|
+
const { nodes, edges } = graph;
|
|
219
|
+
|
|
220
|
+
// Filter to nodes in or related to module
|
|
221
|
+
const moduleNodes = nodes.filter(n =>
|
|
222
|
+
n.name.startsWith(modulePath) ||
|
|
223
|
+
n.name.includes(`/${modulePath}/`)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const moduleNodeIds = new Set(moduleNodes.map(n => n.id));
|
|
227
|
+
|
|
228
|
+
// Include nodes that import or are imported by module nodes
|
|
229
|
+
const relatedEdges = edges.filter(e =>
|
|
230
|
+
moduleNodeIds.has(e.from) || moduleNodeIds.has(e.to)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const relatedNodeIds = new Set();
|
|
234
|
+
for (const edge of relatedEdges) {
|
|
235
|
+
relatedNodeIds.add(edge.from);
|
|
236
|
+
relatedNodeIds.add(edge.to);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const filteredNodes = nodes.filter(n => relatedNodeIds.has(n.id));
|
|
240
|
+
const filteredEdges = edges.filter(e =>
|
|
241
|
+
relatedNodeIds.has(e.from) && relatedNodeIds.has(e.to)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return this.generateFlowchart(
|
|
245
|
+
{ nodes: filteredNodes, edges: filteredEdges, external: [] },
|
|
246
|
+
{ ...options, groupByDirectory: false }
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Generate coupling matrix diagram
|
|
252
|
+
* @param {Object} couplingData - From CouplingCalculator
|
|
253
|
+
* @returns {string} Mermaid diagram
|
|
254
|
+
*/
|
|
255
|
+
generateCouplingMatrix(couplingData) {
|
|
256
|
+
const { matrix, modules } = couplingData;
|
|
257
|
+
|
|
258
|
+
if (!matrix || modules.length === 0) {
|
|
259
|
+
return 'flowchart TD\n empty[No coupling data]\n';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let mermaid = 'flowchart LR\n';
|
|
263
|
+
|
|
264
|
+
// Create nodes for each module
|
|
265
|
+
for (const mod of modules) {
|
|
266
|
+
const nodeId = this.sanitizeId(mod.name);
|
|
267
|
+
const label = `${mod.name}\\nCa:${mod.afferent} Ce:${mod.efferent}`;
|
|
268
|
+
mermaid += ` ${nodeId}[${this.escapeLabel(label)}]\n`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Create edges from matrix
|
|
272
|
+
for (let i = 0; i < modules.length; i++) {
|
|
273
|
+
for (let j = 0; j < modules.length; j++) {
|
|
274
|
+
if (matrix[i][j] > 0) {
|
|
275
|
+
const fromId = this.sanitizeId(modules[i].name);
|
|
276
|
+
const toId = this.sanitizeId(modules[j].name);
|
|
277
|
+
mermaid += ` ${fromId} -->|${matrix[i][j]}| ${toId}\n`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return mermaid;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Generate service boundary diagram
|
|
287
|
+
* @param {Object} boundaryData - From BoundaryDetector
|
|
288
|
+
* @returns {string} Mermaid diagram
|
|
289
|
+
*/
|
|
290
|
+
generateBoundaryDiagram(boundaryData) {
|
|
291
|
+
const { services, shared } = boundaryData;
|
|
292
|
+
|
|
293
|
+
if (!services || services.length === 0) {
|
|
294
|
+
return 'flowchart TD\n empty[No services detected]\n';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let mermaid = 'flowchart TB\n';
|
|
298
|
+
|
|
299
|
+
// Shared kernel
|
|
300
|
+
if (shared && shared.length > 0) {
|
|
301
|
+
mermaid += ' subgraph shared[Shared Kernel]\n';
|
|
302
|
+
for (const file of shared.slice(0, 10)) {
|
|
303
|
+
const nodeId = this.sanitizeId(`shared_${file}`);
|
|
304
|
+
mermaid += ` ${nodeId}[${this.escapeLabel(path.basename(file))}]\n`;
|
|
305
|
+
}
|
|
306
|
+
mermaid += ' end\n\n';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Services
|
|
310
|
+
for (const service of services) {
|
|
311
|
+
const serviceId = this.sanitizeId(`svc_${service.name}`);
|
|
312
|
+
mermaid += ` subgraph ${serviceId}[${this.escapeLabel(service.name)}]\n`;
|
|
313
|
+
|
|
314
|
+
for (const file of (service.files || []).slice(0, 10)) {
|
|
315
|
+
const nodeId = this.sanitizeId(file);
|
|
316
|
+
mermaid += ` ${nodeId}[${this.escapeLabel(path.basename(file))}]\n`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
mermaid += ' end\n';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Service dependencies
|
|
323
|
+
for (const service of services) {
|
|
324
|
+
const fromId = this.sanitizeId(`svc_${service.name}`);
|
|
325
|
+
for (const dep of service.dependencies || []) {
|
|
326
|
+
const toId = this.sanitizeId(`svc_${dep}`);
|
|
327
|
+
mermaid += ` ${fromId} --> ${toId}\n`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return mermaid;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Sanitize string for use as Mermaid ID
|
|
336
|
+
*/
|
|
337
|
+
sanitizeId(str) {
|
|
338
|
+
return str
|
|
339
|
+
.replace(/[^a-zA-Z0-9]/g, '_')
|
|
340
|
+
.replace(/^_+|_+$/g, '')
|
|
341
|
+
.replace(/_+/g, '_')
|
|
342
|
+
|| 'node';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Escape label for Mermaid
|
|
347
|
+
*/
|
|
348
|
+
escapeLabel(str) {
|
|
349
|
+
return str
|
|
350
|
+
.replace(/"/g, "'")
|
|
351
|
+
.replace(/\[/g, '(')
|
|
352
|
+
.replace(/\]/g, ')')
|
|
353
|
+
.replace(/>/g, ')')
|
|
354
|
+
.replace(/</g, '(');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = { MermaidGenerator };
|