@getmikk/diagram-generator 1.2.0 → 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.
@@ -1,108 +1,108 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
-
3
- /**
4
- * FlowDiagramGenerator — generates sequence diagrams for specific
5
- * function call flows (e.g., "show me the HTTP request → response flow").
6
- * Outputs: .mikk/diagrams/flows/{flowName}.mmd
7
- */
8
- export class FlowDiagramGenerator {
9
- constructor(
10
- private lock: MikkLock
11
- ) { }
12
-
13
- /** Generate a sequence diagram starting from a function */
14
- generate(startFunctionId: string, maxDepth: number = 5): string {
15
- const lines: string[] = []
16
- lines.push('sequenceDiagram')
17
- lines.push('')
18
-
19
- const visited = new Set<string>()
20
- this.traceFlow(startFunctionId, lines, visited, 0, maxDepth)
21
-
22
- if (lines.length <= 2) {
23
- lines.push(` Note over ${this.sanitizeId(startFunctionId)}: No outgoing calls found`)
24
- }
25
-
26
- return lines.join('\n')
27
- }
28
-
29
- /** Generate a flow diagram showing all entry points grouped by module */
30
- generateEntryPoints(): string {
31
- const lines: string[] = []
32
- lines.push('graph TD')
33
- lines.push('')
34
-
35
- // Find functions with no callers (entry points)
36
- const allFunctions = Object.values(this.lock.functions)
37
- const entryPoints = allFunctions.filter(fn => fn.calledBy.length === 0)
38
-
39
- // Group entry points by module
40
- const entryByModule = new Map<string, typeof entryPoints>()
41
- for (const fn of entryPoints) {
42
- if (!entryByModule.has(fn.moduleId)) {
43
- entryByModule.set(fn.moduleId, [])
44
- }
45
- entryByModule.get(fn.moduleId)!.push(fn)
46
- }
47
-
48
- for (const [modId, fns] of entryByModule) {
49
- lines.push(` subgraph mod_${this.sanitizeId(modId)}["📦 ${modId}"]`)
50
- for (const fn of fns) {
51
- lines.push(` ${this.sanitizeId(fn.id)}["🚀 ${fn.name}<br/>(Entry)"]`)
52
- }
53
- lines.push(' end')
54
- }
55
-
56
- lines.push('')
57
-
58
- // Show first-level calls from entry points
59
- for (const fn of entryPoints) {
60
- const outEdges = fn.calls
61
- for (const targetId of outEdges) {
62
- const targetFn = this.lock.functions[targetId]
63
- if (targetFn) {
64
- lines.push(` ${this.sanitizeId(fn.id)} --> ${this.sanitizeId(targetId)}["${targetFn.name}"]`)
65
- }
66
- }
67
- }
68
-
69
- lines.push('')
70
- lines.push(' classDef default fill:#f9f9f9,stroke:#333')
71
-
72
- return lines.join('\n')
73
- }
74
-
75
- private traceFlow(
76
- fnId: string,
77
- lines: string[],
78
- visited: Set<string>,
79
- depth: number,
80
- maxDepth: number
81
- ): void {
82
- if (depth >= maxDepth || visited.has(fnId)) return
83
- visited.add(fnId)
84
-
85
- const fn = this.lock.functions[fnId]
86
- if (!fn) return
87
-
88
- const participant = this.getParticipantName(fn.moduleId, fn.name)
89
-
90
- for (const callTarget of fn.calls) {
91
- const targetFn = this.lock.functions[callTarget]
92
- if (!targetFn) continue
93
-
94
- const targetParticipant = this.getParticipantName(targetFn.moduleId, targetFn.name)
95
- lines.push(` ${participant}->>+${targetParticipant}: ${fn.name}() → ${targetFn.name}()`)
96
- this.traceFlow(callTarget, lines, visited, depth + 1, maxDepth)
97
- lines.push(` ${targetParticipant}-->>-${participant}: return`)
98
- }
99
- }
100
-
101
- private getParticipantName(moduleId: string, fnName: string): string {
102
- return this.sanitizeId(`${moduleId}_${fnName}`)
103
- }
104
-
105
- private sanitizeId(id: string): string {
106
- return id.replace(/[^a-zA-Z0-9_]/g, '_')
107
- }
108
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+
3
+ /**
4
+ * FlowDiagramGenerator — generates sequence diagrams for specific
5
+ * function call flows (e.g., "show me the HTTP request → response flow").
6
+ * Outputs: .mikk/diagrams/flows/{flowName}.mmd
7
+ */
8
+ export class FlowDiagramGenerator {
9
+ constructor(
10
+ private lock: MikkLock
11
+ ) { }
12
+
13
+ /** Generate a sequence diagram starting from a function */
14
+ generate(startFunctionId: string, maxDepth: number = 5): string {
15
+ const lines: string[] = []
16
+ lines.push('sequenceDiagram')
17
+ lines.push('')
18
+
19
+ const visited = new Set<string>()
20
+ this.traceFlow(startFunctionId, lines, visited, 0, maxDepth)
21
+
22
+ if (lines.length <= 2) {
23
+ lines.push(` Note over ${this.sanitizeId(startFunctionId)}: No outgoing calls found`)
24
+ }
25
+
26
+ return lines.join('\n')
27
+ }
28
+
29
+ /** Generate a flow diagram showing all entry points grouped by module */
30
+ generateEntryPoints(): string {
31
+ const lines: string[] = []
32
+ lines.push('graph TD')
33
+ lines.push('')
34
+
35
+ // Find functions with no callers (entry points)
36
+ const allFunctions = Object.values(this.lock.functions)
37
+ const entryPoints = allFunctions.filter(fn => fn.calledBy.length === 0)
38
+
39
+ // Group entry points by module
40
+ const entryByModule = new Map<string, typeof entryPoints>()
41
+ for (const fn of entryPoints) {
42
+ if (!entryByModule.has(fn.moduleId)) {
43
+ entryByModule.set(fn.moduleId, [])
44
+ }
45
+ entryByModule.get(fn.moduleId)!.push(fn)
46
+ }
47
+
48
+ for (const [modId, fns] of entryByModule) {
49
+ lines.push(` subgraph mod_${this.sanitizeId(modId)}["📦 ${modId}"]`)
50
+ for (const fn of fns) {
51
+ lines.push(` ${this.sanitizeId(fn.id)}["🚀 ${fn.name}<br/>(Entry)"]`)
52
+ }
53
+ lines.push(' end')
54
+ }
55
+
56
+ lines.push('')
57
+
58
+ // Show first-level calls from entry points
59
+ for (const fn of entryPoints) {
60
+ const outEdges = fn.calls
61
+ for (const targetId of outEdges) {
62
+ const targetFn = this.lock.functions[targetId]
63
+ if (targetFn) {
64
+ lines.push(` ${this.sanitizeId(fn.id)} --> ${this.sanitizeId(targetId)}["${targetFn.name}"]`)
65
+ }
66
+ }
67
+ }
68
+
69
+ lines.push('')
70
+ lines.push(' classDef default fill:#f9f9f9,stroke:#333')
71
+
72
+ return lines.join('\n')
73
+ }
74
+
75
+ private traceFlow(
76
+ fnId: string,
77
+ lines: string[],
78
+ visited: Set<string>,
79
+ depth: number,
80
+ maxDepth: number
81
+ ): void {
82
+ if (depth >= maxDepth || visited.has(fnId)) return
83
+ visited.add(fnId)
84
+
85
+ const fn = this.lock.functions[fnId]
86
+ if (!fn) return
87
+
88
+ const participant = this.getParticipantName(fn.moduleId, fn.name)
89
+
90
+ for (const callTarget of fn.calls) {
91
+ const targetFn = this.lock.functions[callTarget]
92
+ if (!targetFn) continue
93
+
94
+ const targetParticipant = this.getParticipantName(targetFn.moduleId, targetFn.name)
95
+ lines.push(` ${participant}->>+${targetParticipant}: ${fn.name}() → ${targetFn.name}()`)
96
+ this.traceFlow(callTarget, lines, visited, depth + 1, maxDepth)
97
+ lines.push(` ${targetParticipant}-->>-${participant}: return`)
98
+ }
99
+ }
100
+
101
+ private getParticipantName(moduleId: string, fnName: string): string {
102
+ return this.sanitizeId(`${moduleId}_${fnName}`)
103
+ }
104
+
105
+ private sanitizeId(id: string): string {
106
+ return id.replace(/[^a-zA-Z0-9_]/g, '_')
107
+ }
108
+ }
@@ -1,88 +1,88 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
-
3
- /**
4
- * HealthDiagramGenerator — generates a module health dashboard showing
5
- * coupling, cohesion, and complexity metrics.
6
- * Outputs: .mikk/diagrams/health.mmd
7
- */
8
- export class HealthDiagramGenerator {
9
- constructor(
10
- private contract: MikkContract,
11
- private lock: MikkLock
12
- ) { }
13
-
14
- generate(): string {
15
- const lines: string[] = []
16
- lines.push('graph TD')
17
- lines.push('')
18
-
19
- const classAssignments: string[] = []
20
-
21
- for (const module of this.contract.declared.modules) {
22
- const metrics = this.computeMetrics(module.id)
23
- const healthIcon = metrics.health > 0.7 ? '🟢' : metrics.health > 0.4 ? '🟡' : '🔴'
24
- const healthClass = metrics.health > 0.7 ? 'healthy' : metrics.health > 0.4 ? 'warning' : 'critical'
25
- const sid = this.sanitizeId(module.id)
26
-
27
- lines.push(` ${sid}["${healthIcon} ${module.name}<br/>Cohesion: ${(metrics.cohesion * 100).toFixed(0)}%<br/>Coupling: ${metrics.coupling}<br/>Functions: ${metrics.functionCount}"]`)
28
- classAssignments.push(` class ${sid} ${healthClass}`)
29
- }
30
-
31
- // Add inter-module dependency edges for context
32
- const moduleEdges = new Set<string>()
33
- for (const fn of Object.values(this.lock.functions)) {
34
- for (const callTarget of fn.calls) {
35
- const targetFn = this.lock.functions[callTarget]
36
- if (targetFn && fn.moduleId !== targetFn.moduleId) {
37
- const key = `${fn.moduleId}|${targetFn.moduleId}`
38
- if (!moduleEdges.has(key)) {
39
- moduleEdges.add(key)
40
- lines.push(` ${this.sanitizeId(fn.moduleId)} -.-> ${this.sanitizeId(targetFn.moduleId)}`)
41
- }
42
- }
43
- }
44
- }
45
-
46
- lines.push('')
47
- lines.push(' classDef healthy fill:#27ae60,stroke:#2c3e50,color:#fff')
48
- lines.push(' classDef warning fill:#f39c12,stroke:#2c3e50,color:#fff')
49
- lines.push(' classDef critical fill:#e74c3c,stroke:#2c3e50,color:#fff')
50
- lines.push('')
51
- for (const assignment of classAssignments) {
52
- lines.push(assignment)
53
- }
54
-
55
- return lines.join('\n')
56
- }
57
-
58
- private computeMetrics(moduleId: string) {
59
- const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
60
- const functionCount = moduleFunctions.length
61
-
62
- // Coupling: count of external calls
63
- let externalCalls = 0
64
- let internalCalls = 0
65
- for (const fn of moduleFunctions) {
66
- for (const call of fn.calls) {
67
- const target = this.lock.functions[call]
68
- if (target) {
69
- if (target.moduleId === moduleId) internalCalls++
70
- else externalCalls++
71
- }
72
- }
73
- }
74
-
75
- // Cohesion: ratio of internal to total calls
76
- const totalCalls = internalCalls + externalCalls
77
- const cohesion = totalCalls === 0 ? 0.5 : internalCalls / totalCalls
78
-
79
- // Health: weighted score
80
- const health = cohesion * 0.6 + (functionCount > 0 ? Math.min(1, 10 / functionCount) * 0.4 : 0.4)
81
-
82
- return { cohesion, coupling: externalCalls, functionCount, health }
83
- }
84
-
85
- private sanitizeId(id: string): string {
86
- return id.replace(/[^a-zA-Z0-9_]/g, '_')
87
- }
88
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+
3
+ /**
4
+ * HealthDiagramGenerator — generates a module health dashboard showing
5
+ * coupling, cohesion, and complexity metrics.
6
+ * Outputs: .mikk/diagrams/health.mmd
7
+ */
8
+ export class HealthDiagramGenerator {
9
+ constructor(
10
+ private contract: MikkContract,
11
+ private lock: MikkLock
12
+ ) { }
13
+
14
+ generate(): string {
15
+ const lines: string[] = []
16
+ lines.push('graph TD')
17
+ lines.push('')
18
+
19
+ const classAssignments: string[] = []
20
+
21
+ for (const module of this.contract.declared.modules) {
22
+ const metrics = this.computeMetrics(module.id)
23
+ const healthIcon = metrics.health > 0.7 ? '🟢' : metrics.health > 0.4 ? '🟡' : '🔴'
24
+ const healthClass = metrics.health > 0.7 ? 'healthy' : metrics.health > 0.4 ? 'warning' : 'critical'
25
+ const sid = this.sanitizeId(module.id)
26
+
27
+ lines.push(` ${sid}["${healthIcon} ${module.name}<br/>Cohesion: ${(metrics.cohesion * 100).toFixed(0)}%<br/>Coupling: ${metrics.coupling}<br/>Functions: ${metrics.functionCount}"]`)
28
+ classAssignments.push(` class ${sid} ${healthClass}`)
29
+ }
30
+
31
+ // Add inter-module dependency edges for context
32
+ const moduleEdges = new Set<string>()
33
+ for (const fn of Object.values(this.lock.functions)) {
34
+ for (const callTarget of fn.calls) {
35
+ const targetFn = this.lock.functions[callTarget]
36
+ if (targetFn && fn.moduleId !== targetFn.moduleId) {
37
+ const key = `${fn.moduleId}|${targetFn.moduleId}`
38
+ if (!moduleEdges.has(key)) {
39
+ moduleEdges.add(key)
40
+ lines.push(` ${this.sanitizeId(fn.moduleId)} -.-> ${this.sanitizeId(targetFn.moduleId)}`)
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ lines.push('')
47
+ lines.push(' classDef healthy fill:#27ae60,stroke:#2c3e50,color:#fff')
48
+ lines.push(' classDef warning fill:#f39c12,stroke:#2c3e50,color:#fff')
49
+ lines.push(' classDef critical fill:#e74c3c,stroke:#2c3e50,color:#fff')
50
+ lines.push('')
51
+ for (const assignment of classAssignments) {
52
+ lines.push(assignment)
53
+ }
54
+
55
+ return lines.join('\n')
56
+ }
57
+
58
+ private computeMetrics(moduleId: string) {
59
+ const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
60
+ const functionCount = moduleFunctions.length
61
+
62
+ // Coupling: count of external calls
63
+ let externalCalls = 0
64
+ let internalCalls = 0
65
+ for (const fn of moduleFunctions) {
66
+ for (const call of fn.calls) {
67
+ const target = this.lock.functions[call]
68
+ if (target) {
69
+ if (target.moduleId === moduleId) internalCalls++
70
+ else externalCalls++
71
+ }
72
+ }
73
+ }
74
+
75
+ // Cohesion: ratio of internal to total calls
76
+ const totalCalls = internalCalls + externalCalls
77
+ const cohesion = totalCalls === 0 ? 0.5 : internalCalls / totalCalls
78
+
79
+ // Health: weighted score
80
+ const health = cohesion * 0.6 + (functionCount > 0 ? Math.min(1, 10 / functionCount) * 0.4 : 0.4)
81
+
82
+ return { cohesion, coupling: externalCalls, functionCount, health }
83
+ }
84
+
85
+ private sanitizeId(id: string): string {
86
+ return id.replace(/[^a-zA-Z0-9_]/g, '_')
87
+ }
88
+ }
@@ -1,65 +1,65 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
-
3
- /**
4
- * ImpactDiagramGenerator — generates a Mermaid diagram showing the
5
- * impact of changes in specific files/functions.
6
- * Outputs: .mikk/diagrams/impact/{filename}.mmd
7
- */
8
- export class ImpactDiagramGenerator {
9
- constructor(
10
- private lock: MikkLock
11
- ) { }
12
-
13
- generate(changedNodeIds: string[], impactedNodeIds: string[]): string {
14
- const lines: string[] = []
15
- lines.push('graph LR')
16
- lines.push('')
17
-
18
- // Changed nodes (red)
19
- for (const id of changedNodeIds) {
20
- const fn = this.lock.functions[id]
21
- if (fn) {
22
- lines.push(` ${this.sanitizeId(id)}["🔴 ${fn.name}<br/>${fn.file}"]`)
23
- }
24
- }
25
-
26
- // Impacted nodes (orange)
27
- for (const id of impactedNodeIds) {
28
- const fn = this.lock.functions[id]
29
- if (fn) {
30
- lines.push(` ${this.sanitizeId(id)}["🟠 ${fn.name}<br/>${fn.file}"]`)
31
- }
32
- }
33
-
34
- lines.push('')
35
-
36
- // Draw edges showing the impact chain
37
- const allIds = new Set([...changedNodeIds, ...impactedNodeIds])
38
- for (const id of allIds) {
39
- const fn = this.lock.functions[id]
40
- if (!fn) continue
41
- for (const callerId of fn.calledBy) {
42
- if (allIds.has(callerId)) {
43
- lines.push(` ${this.sanitizeId(callerId)} --> ${this.sanitizeId(id)}`)
44
- }
45
- }
46
- }
47
-
48
- lines.push('')
49
- lines.push(' classDef changed fill:#e74c3c,stroke:#c0392b,color:#fff')
50
- lines.push(' classDef impacted fill:#e67e22,stroke:#d35400,color:#fff')
51
-
52
- for (const id of changedNodeIds) {
53
- lines.push(` class ${this.sanitizeId(id)} changed`)
54
- }
55
- for (const id of impactedNodeIds) {
56
- lines.push(` class ${this.sanitizeId(id)} impacted`)
57
- }
58
-
59
- return lines.join('\n')
60
- }
61
-
62
- private sanitizeId(id: string): string {
63
- return id.replace(/[^a-zA-Z0-9_]/g, '_')
64
- }
65
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+
3
+ /**
4
+ * ImpactDiagramGenerator — generates a Mermaid diagram showing the
5
+ * impact of changes in specific files/functions.
6
+ * Outputs: .mikk/diagrams/impact/{filename}.mmd
7
+ */
8
+ export class ImpactDiagramGenerator {
9
+ constructor(
10
+ private lock: MikkLock
11
+ ) { }
12
+
13
+ generate(changedNodeIds: string[], impactedNodeIds: string[]): string {
14
+ const lines: string[] = []
15
+ lines.push('graph LR')
16
+ lines.push('')
17
+
18
+ // Changed nodes (red)
19
+ for (const id of changedNodeIds) {
20
+ const fn = this.lock.functions[id]
21
+ if (fn) {
22
+ lines.push(` ${this.sanitizeId(id)}["🔴 ${fn.name}<br/>${fn.file}"]`)
23
+ }
24
+ }
25
+
26
+ // Impacted nodes (orange)
27
+ for (const id of impactedNodeIds) {
28
+ const fn = this.lock.functions[id]
29
+ if (fn) {
30
+ lines.push(` ${this.sanitizeId(id)}["🟠 ${fn.name}<br/>${fn.file}"]`)
31
+ }
32
+ }
33
+
34
+ lines.push('')
35
+
36
+ // Draw edges showing the impact chain
37
+ const allIds = new Set([...changedNodeIds, ...impactedNodeIds])
38
+ for (const id of allIds) {
39
+ const fn = this.lock.functions[id]
40
+ if (!fn) continue
41
+ for (const callerId of fn.calledBy) {
42
+ if (allIds.has(callerId)) {
43
+ lines.push(` ${this.sanitizeId(callerId)} --> ${this.sanitizeId(id)}`)
44
+ }
45
+ }
46
+ }
47
+
48
+ lines.push('')
49
+ lines.push(' classDef changed fill:#e74c3c,stroke:#c0392b,color:#fff')
50
+ lines.push(' classDef impacted fill:#e67e22,stroke:#d35400,color:#fff')
51
+
52
+ for (const id of changedNodeIds) {
53
+ lines.push(` class ${this.sanitizeId(id)} changed`)
54
+ }
55
+ for (const id of impactedNodeIds) {
56
+ lines.push(` class ${this.sanitizeId(id)} impacted`)
57
+ }
58
+
59
+ return lines.join('\n')
60
+ }
61
+
62
+ private sanitizeId(id: string): string {
63
+ return id.replace(/[^a-zA-Z0-9_]/g, '_')
64
+ }
65
+ }
@@ -1,63 +1,63 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
-
3
- /**
4
- * MainDiagramGenerator — generates the top-level Mermaid diagram
5
- * showing all modules and their interconnections.
6
- * Outputs: .mikk/diagrams/main.mmd
7
- */
8
- export class MainDiagramGenerator {
9
- constructor(
10
- private contract: MikkContract,
11
- private lock: MikkLock
12
- ) { }
13
-
14
- generate(): string {
15
- const lines: string[] = []
16
- lines.push('graph TD')
17
- lines.push('')
18
-
19
- // Add module nodes
20
- for (const module of this.contract.declared.modules) {
21
- const lockModule = this.lock.modules[module.id]
22
- const fileCount = lockModule?.files.length || 0
23
- const fnCount = Object.values(this.lock.functions).filter(f => f.moduleId === module.id).length
24
- lines.push(` ${this.sanitizeId(module.id)}["📦 ${module.name}<br/>${fileCount} files, ${fnCount} functions"]`)
25
- }
26
-
27
- lines.push('')
28
-
29
- // Add inter-module edges based on cross-module function calls
30
- const moduleEdges = new Map<string, Set<string>>()
31
-
32
- for (const fn of Object.values(this.lock.functions)) {
33
- for (const callTarget of fn.calls) {
34
- const targetFn = this.lock.functions[callTarget]
35
- if (targetFn && fn.moduleId !== targetFn.moduleId) {
36
- const edgeKey = `${fn.moduleId}→${targetFn.moduleId}`
37
- if (!moduleEdges.has(edgeKey)) {
38
- moduleEdges.set(edgeKey, new Set())
39
- }
40
- moduleEdges.get(edgeKey)!.add(`${fn.name}→${targetFn.name}`)
41
- }
42
- }
43
- }
44
-
45
- for (const [edge, calls] of moduleEdges) {
46
- const [from, to] = edge.split('→')
47
- lines.push(` ${this.sanitizeId(from)} -->|${calls.size} calls| ${this.sanitizeId(to)}`)
48
- }
49
-
50
- // Style
51
- lines.push('')
52
- lines.push(' classDef module fill:#4a90d9,stroke:#2c3e50,color:#fff,stroke-width:2px')
53
- for (const module of this.contract.declared.modules) {
54
- lines.push(` class ${this.sanitizeId(module.id)} module`)
55
- }
56
-
57
- return lines.join('\n')
58
- }
59
-
60
- private sanitizeId(id: string): string {
61
- return id.replace(/[^a-zA-Z0-9_]/g, '_')
62
- }
63
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+
3
+ /**
4
+ * MainDiagramGenerator — generates the top-level Mermaid diagram
5
+ * showing all modules and their interconnections.
6
+ * Outputs: .mikk/diagrams/main.mmd
7
+ */
8
+ export class MainDiagramGenerator {
9
+ constructor(
10
+ private contract: MikkContract,
11
+ private lock: MikkLock
12
+ ) { }
13
+
14
+ generate(): string {
15
+ const lines: string[] = []
16
+ lines.push('graph TD')
17
+ lines.push('')
18
+
19
+ // Add module nodes
20
+ for (const module of this.contract.declared.modules) {
21
+ const lockModule = this.lock.modules[module.id]
22
+ const fileCount = lockModule?.files.length || 0
23
+ const fnCount = Object.values(this.lock.functions).filter(f => f.moduleId === module.id).length
24
+ lines.push(` ${this.sanitizeId(module.id)}["📦 ${module.name}<br/>${fileCount} files, ${fnCount} functions"]`)
25
+ }
26
+
27
+ lines.push('')
28
+
29
+ // Add inter-module edges based on cross-module function calls
30
+ const moduleEdges = new Map<string, Set<string>>()
31
+
32
+ for (const fn of Object.values(this.lock.functions)) {
33
+ for (const callTarget of fn.calls) {
34
+ const targetFn = this.lock.functions[callTarget]
35
+ if (targetFn && fn.moduleId !== targetFn.moduleId) {
36
+ const edgeKey = `${fn.moduleId}→${targetFn.moduleId}`
37
+ if (!moduleEdges.has(edgeKey)) {
38
+ moduleEdges.set(edgeKey, new Set())
39
+ }
40
+ moduleEdges.get(edgeKey)!.add(`${fn.name}→${targetFn.name}`)
41
+ }
42
+ }
43
+ }
44
+
45
+ for (const [edge, calls] of moduleEdges) {
46
+ const [from, to] = edge.split('→')
47
+ lines.push(` ${this.sanitizeId(from)} -->|${calls.size} calls| ${this.sanitizeId(to)}`)
48
+ }
49
+
50
+ // Style
51
+ lines.push('')
52
+ lines.push(' classDef module fill:#4a90d9,stroke:#2c3e50,color:#fff,stroke-width:2px')
53
+ for (const module of this.contract.declared.modules) {
54
+ lines.push(` class ${this.sanitizeId(module.id)} module`)
55
+ }
56
+
57
+ return lines.join('\n')
58
+ }
59
+
60
+ private sanitizeId(id: string): string {
61
+ return id.replace(/[^a-zA-Z0-9_]/g, '_')
62
+ }
63
+ }