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.
Files changed (87) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  3. package/dashboard/dist/components/UsagePane.js +51 -0
  4. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  5. package/dashboard/dist/components/UsagePane.test.js +142 -0
  6. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  7. package/dashboard/dist/components/WorkspaceDocsPane.js +146 -0
  8. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  10. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  11. package/dashboard/dist/components/WorkspacePane.js +17 -0
  12. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  13. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  14. package/package.json +15 -4
  15. package/scripts/capture-screenshots.js +170 -0
  16. package/scripts/docs-update.js +253 -0
  17. package/scripts/generate-screenshots.js +321 -0
  18. package/scripts/project-docs.js +377 -0
  19. package/scripts/vps-setup.sh +477 -0
  20. package/server/lib/architecture-command.js +450 -0
  21. package/server/lib/architecture-command.test.js +754 -0
  22. package/server/lib/ast-analyzer.js +324 -0
  23. package/server/lib/ast-analyzer.test.js +437 -0
  24. package/server/lib/auth-system.test.js +4 -1
  25. package/server/lib/boundary-detector.js +427 -0
  26. package/server/lib/boundary-detector.test.js +320 -0
  27. package/server/lib/budget-alerts.js +138 -0
  28. package/server/lib/budget-alerts.test.js +235 -0
  29. package/server/lib/candidates-tracker.js +210 -0
  30. package/server/lib/candidates-tracker.test.js +300 -0
  31. package/server/lib/checkpoint-manager.js +251 -0
  32. package/server/lib/checkpoint-manager.test.js +474 -0
  33. package/server/lib/circular-detector.js +337 -0
  34. package/server/lib/circular-detector.test.js +353 -0
  35. package/server/lib/cohesion-analyzer.js +310 -0
  36. package/server/lib/cohesion-analyzer.test.js +447 -0
  37. package/server/lib/contract-testing.js +625 -0
  38. package/server/lib/contract-testing.test.js +342 -0
  39. package/server/lib/conversion-planner.js +469 -0
  40. package/server/lib/conversion-planner.test.js +361 -0
  41. package/server/lib/convert-command.js +351 -0
  42. package/server/lib/convert-command.test.js +608 -0
  43. package/server/lib/coupling-calculator.js +189 -0
  44. package/server/lib/coupling-calculator.test.js +509 -0
  45. package/server/lib/dependency-graph.js +367 -0
  46. package/server/lib/dependency-graph.test.js +516 -0
  47. package/server/lib/duplication-detector.js +349 -0
  48. package/server/lib/duplication-detector.test.js +401 -0
  49. package/server/lib/example-service.js +616 -0
  50. package/server/lib/example-service.test.js +397 -0
  51. package/server/lib/impact-scorer.js +184 -0
  52. package/server/lib/impact-scorer.test.js +211 -0
  53. package/server/lib/mermaid-generator.js +358 -0
  54. package/server/lib/mermaid-generator.test.js +301 -0
  55. package/server/lib/messaging-patterns.js +750 -0
  56. package/server/lib/messaging-patterns.test.js +213 -0
  57. package/server/lib/microservice-template.js +386 -0
  58. package/server/lib/microservice-template.test.js +325 -0
  59. package/server/lib/new-project-microservice.js +450 -0
  60. package/server/lib/new-project-microservice.test.js +600 -0
  61. package/server/lib/refactor-command.js +326 -0
  62. package/server/lib/refactor-command.test.js +528 -0
  63. package/server/lib/refactor-executor.js +254 -0
  64. package/server/lib/refactor-executor.test.js +305 -0
  65. package/server/lib/refactor-observer.js +292 -0
  66. package/server/lib/refactor-observer.test.js +422 -0
  67. package/server/lib/refactor-progress.js +193 -0
  68. package/server/lib/refactor-progress.test.js +251 -0
  69. package/server/lib/refactor-reporter.js +237 -0
  70. package/server/lib/refactor-reporter.test.js +247 -0
  71. package/server/lib/semantic-analyzer.js +198 -0
  72. package/server/lib/semantic-analyzer.test.js +474 -0
  73. package/server/lib/service-scaffold.js +486 -0
  74. package/server/lib/service-scaffold.test.js +373 -0
  75. package/server/lib/shared-kernel.js +578 -0
  76. package/server/lib/shared-kernel.test.js +255 -0
  77. package/server/lib/traefik-config.js +282 -0
  78. package/server/lib/traefik-config.test.js +312 -0
  79. package/server/lib/usage-command.js +218 -0
  80. package/server/lib/usage-command.test.js +391 -0
  81. package/server/lib/usage-formatter.js +192 -0
  82. package/server/lib/usage-formatter.test.js +267 -0
  83. package/server/lib/usage-history.js +122 -0
  84. package/server/lib/usage-history.test.js +206 -0
  85. package/server/package-lock.json +14 -0
  86. package/server/package.json +1 -0
  87. 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 };