@getmikk/diagram-generator 1.3.1 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikk/diagram-generator",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,7 +21,7 @@
21
21
  "dev": "tsc --watch"
22
22
  },
23
23
  "dependencies": {
24
- "@getmikk/core": "^1.3.1"
24
+ "@getmikk/core": "^1.5.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "typescript": "^5.7.0",
@@ -16,6 +16,7 @@ export class CapsuleDiagramGenerator {
16
16
  if (!module) return `%% Module "${moduleId}" not found`
17
17
 
18
18
  const lines: string[] = []
19
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
19
20
  lines.push('graph LR')
20
21
  lines.push('')
21
22
 
@@ -67,8 +68,22 @@ export class CapsuleDiagramGenerator {
67
68
  }
68
69
 
69
70
  lines.push('')
70
- lines.push(' classDef publicApi fill:#27ae60,stroke:#2c3e50,color:#fff')
71
- lines.push(' classDef internalApi fill:#7f8c8d,stroke:#2c3e50,color:#fff')
71
+ lines.push(' classDef publicApi fill:#22c55e,stroke:#4ade80,color:#f0fdf4')
72
+ lines.push(' classDef internalApi fill:#64748b,stroke:#94a3b8,color:#f1f5f9')
73
+
74
+ // Apply classes to nodes
75
+ for (const fn of exportedFns) {
76
+ lines.push(` class ${this.sanitizeId(fn.id)} publicApi`)
77
+ }
78
+ for (const fn of internalFns) {
79
+ if (internalFns.length <= 10) {
80
+ lines.push(` class ${this.sanitizeId(fn.id)} internalApi`)
81
+ }
82
+ }
83
+ // Apply to the summary node if used
84
+ if (internalFns.length > 10) {
85
+ lines.push(' class internal internalApi')
86
+ }
72
87
 
73
88
  return lines.join('\n')
74
89
  }
@@ -23,6 +23,7 @@ export class DependencyMatrixGenerator {
23
23
  const matrix = this.computeMatrix()
24
24
  const lines: string[] = []
25
25
 
26
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
26
27
  lines.push('graph LR')
27
28
  lines.push('')
28
29
 
@@ -44,7 +45,7 @@ export class DependencyMatrixGenerator {
44
45
  }
45
46
 
46
47
  lines.push('')
47
- lines.push(' classDef default fill:#ecf0f1,stroke:#34495e,color:#2c3e50')
48
+ lines.push(' classDef default fill:#334155,stroke:#64748b,color:#e2e8f0')
48
49
 
49
50
  return lines.join('\n')
50
51
  }
@@ -78,6 +79,7 @@ export class DependencyMatrixGenerator {
78
79
  private computeMatrix(): Map<string, number> {
79
80
  const counts = new Map<string, number>()
80
81
 
82
+ // Count function-level cross-module calls
81
83
  for (const fn of Object.values(this.lock.functions)) {
82
84
  for (const callTarget of fn.calls) {
83
85
  const targetFn = this.lock.functions[callTarget]
@@ -88,6 +90,18 @@ export class DependencyMatrixGenerator {
88
90
  }
89
91
  }
90
92
 
93
+ // Count file-level cross-module imports
94
+ for (const file of Object.values(this.lock.files)) {
95
+ if (!file.imports) continue
96
+ for (const importedPath of file.imports) {
97
+ const importedFile = this.lock.files[importedPath]
98
+ if (importedFile && file.moduleId !== importedFile.moduleId) {
99
+ const key = `${file.moduleId}|${importedFile.moduleId}`
100
+ counts.set(key, (counts.get(key) || 0) + 1)
101
+ }
102
+ }
103
+ }
104
+
91
105
  return counts
92
106
  }
93
107
 
@@ -13,6 +13,7 @@ export class FlowDiagramGenerator {
13
13
  /** Generate a sequence diagram starting from a function */
14
14
  generate(startFunctionId: string, maxDepth: number = 5): string {
15
15
  const lines: string[] = []
16
+ lines.push('%%{init: {"theme": "dark"}}%%')
16
17
  lines.push('sequenceDiagram')
17
18
  lines.push('')
18
19
 
@@ -29,6 +30,7 @@ export class FlowDiagramGenerator {
29
30
  /** Generate a flow diagram showing all entry points grouped by module */
30
31
  generateEntryPoints(): string {
31
32
  const lines: string[] = []
33
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
32
34
  lines.push('graph TD')
33
35
  lines.push('')
34
36
 
@@ -67,7 +69,7 @@ export class FlowDiagramGenerator {
67
69
  }
68
70
 
69
71
  lines.push('')
70
- lines.push(' classDef default fill:#f9f9f9,stroke:#333')
72
+ lines.push(' classDef default fill:#334155,stroke:#64748b,color:#e2e8f0')
71
73
 
72
74
  return lines.join('\n')
73
75
  }
@@ -13,6 +13,7 @@ export class HealthDiagramGenerator {
13
13
 
14
14
  generate(): string {
15
15
  const lines: string[] = []
16
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
16
17
  lines.push('graph TD')
17
18
  lines.push('')
18
19
 
@@ -28,7 +29,7 @@ export class HealthDiagramGenerator {
28
29
  classAssignments.push(` class ${sid} ${healthClass}`)
29
30
  }
30
31
 
31
- // Add inter-module dependency edges for context
32
+ // Add inter-module dependency edges for context (function calls + file imports)
32
33
  const moduleEdges = new Set<string>()
33
34
  for (const fn of Object.values(this.lock.functions)) {
34
35
  for (const callTarget of fn.calls) {
@@ -42,11 +43,24 @@ export class HealthDiagramGenerator {
42
43
  }
43
44
  }
44
45
  }
46
+ for (const file of Object.values(this.lock.files)) {
47
+ if (!file.imports) continue
48
+ for (const importedPath of file.imports) {
49
+ const importedFile = this.lock.files[importedPath]
50
+ if (importedFile && file.moduleId !== importedFile.moduleId) {
51
+ const key = `${file.moduleId}|${importedFile.moduleId}`
52
+ if (!moduleEdges.has(key)) {
53
+ moduleEdges.add(key)
54
+ lines.push(` ${this.sanitizeId(file.moduleId)} -.-> ${this.sanitizeId(importedFile.moduleId)}`)
55
+ }
56
+ }
57
+ }
58
+ }
45
59
 
46
60
  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')
61
+ lines.push(' classDef healthy fill:#22c55e,stroke:#4ade80,color:#f0fdf4')
62
+ lines.push(' classDef warning fill:#f59e0b,stroke:#fbbf24,color:#1e293b')
63
+ lines.push(' classDef critical fill:#ef4444,stroke:#f87171,color:#fef2f2')
50
64
  lines.push('')
51
65
  for (const assignment of classAssignments) {
52
66
  lines.push(assignment)
@@ -57,9 +71,10 @@ export class HealthDiagramGenerator {
57
71
 
58
72
  private computeMetrics(moduleId: string) {
59
73
  const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
74
+ const moduleFiles = Object.values(this.lock.files).filter(f => f.moduleId === moduleId)
60
75
  const functionCount = moduleFunctions.length
61
76
 
62
- // Coupling: count of external calls
77
+ // Coupling: count of external calls + external file imports
63
78
  let externalCalls = 0
64
79
  let internalCalls = 0
65
80
  for (const fn of moduleFunctions) {
@@ -71,6 +86,16 @@ export class HealthDiagramGenerator {
71
86
  }
72
87
  }
73
88
  }
89
+ for (const file of moduleFiles) {
90
+ if (!file.imports) continue
91
+ for (const importedPath of file.imports) {
92
+ const importedFile = this.lock.files[importedPath]
93
+ if (importedFile) {
94
+ if (importedFile.moduleId === moduleId) internalCalls++
95
+ else externalCalls++
96
+ }
97
+ }
98
+ }
74
99
 
75
100
  // Cohesion: ratio of internal to total calls
76
101
  const totalCalls = internalCalls + externalCalls
@@ -12,6 +12,7 @@ export class ImpactDiagramGenerator {
12
12
 
13
13
  generate(changedNodeIds: string[], impactedNodeIds: string[]): string {
14
14
  const lines: string[] = []
15
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
15
16
  lines.push('graph LR')
16
17
  lines.push('')
17
18
 
@@ -46,8 +47,8 @@ export class ImpactDiagramGenerator {
46
47
  }
47
48
 
48
49
  lines.push('')
49
- lines.push(' classDef changed fill:#e74c3c,stroke:#c0392b,color:#fff')
50
- lines.push(' classDef impacted fill:#e67e22,stroke:#d35400,color:#fff')
50
+ lines.push(' classDef changed fill:#ef4444,stroke:#f87171,color:#fef2f2')
51
+ lines.push(' classDef impacted fill:#f59e0b,stroke:#fbbf24,color:#1e293b')
51
52
 
52
53
  for (const id of changedNodeIds) {
53
54
  lines.push(` class ${this.sanitizeId(id)} changed`)
@@ -1,8 +1,10 @@
1
+ import * as path from 'node:path'
1
2
  import type { MikkContract, MikkLock } from '@getmikk/core'
2
3
 
3
4
  /**
4
5
  * MainDiagramGenerator — generates the top-level Mermaid diagram
5
6
  * showing all modules and their interconnections.
7
+ * For single-module projects, shows a file-level view instead.
6
8
  * Outputs: .mikk/diagrams/main.mmd
7
9
  */
8
10
  export class MainDiagramGenerator {
@@ -12,7 +14,20 @@ export class MainDiagramGenerator {
12
14
  ) { }
13
15
 
14
16
  generate(): string {
17
+ const modules = this.contract.declared.modules
18
+
19
+ // Single module or no modules — show a file-level view instead of one useless node
20
+ if (modules.length <= 1) {
21
+ return this.generateFileLevelView()
22
+ }
23
+
24
+ return this.generateModuleLevelView()
25
+ }
26
+
27
+ /** Standard multi-module view — modules as nodes, inter-module calls as edges */
28
+ private generateModuleLevelView(): string {
15
29
  const lines: string[] = []
30
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
16
31
  lines.push('graph TD')
17
32
  lines.push('')
18
33
 
@@ -26,9 +41,10 @@ export class MainDiagramGenerator {
26
41
 
27
42
  lines.push('')
28
43
 
29
- // Add inter-module edges based on cross-module function calls
44
+ // Add inter-module edges based on cross-module function calls AND file imports
30
45
  const moduleEdges = new Map<string, Set<string>>()
31
46
 
47
+ // Function-level cross-module calls
32
48
  for (const fn of Object.values(this.lock.functions)) {
33
49
  for (const callTarget of fn.calls) {
34
50
  const targetFn = this.lock.functions[callTarget]
@@ -42,6 +58,23 @@ export class MainDiagramGenerator {
42
58
  }
43
59
  }
44
60
 
61
+ // File-level cross-module imports
62
+ for (const file of Object.values(this.lock.files)) {
63
+ if (!file.imports) continue
64
+ for (const importedPath of file.imports) {
65
+ const importedFile = this.lock.files[importedPath]
66
+ if (importedFile && file.moduleId !== importedFile.moduleId) {
67
+ const edgeKey = `${file.moduleId}→${importedFile.moduleId}`
68
+ if (!moduleEdges.has(edgeKey)) {
69
+ moduleEdges.set(edgeKey, new Set())
70
+ }
71
+ const fromName = file.path.split('/').pop() || file.path
72
+ const toName = importedFile.path.split('/').pop() || importedFile.path
73
+ moduleEdges.get(edgeKey)!.add(`${fromName}→${toName}`)
74
+ }
75
+ }
76
+ }
77
+
45
78
  for (const [edge, calls] of moduleEdges) {
46
79
  const [from, to] = edge.split('→')
47
80
  lines.push(` ${this.sanitizeId(from)} -->|${calls.size} calls| ${this.sanitizeId(to)}`)
@@ -49,7 +82,7 @@ export class MainDiagramGenerator {
49
82
 
50
83
  // Style
51
84
  lines.push('')
52
- lines.push(' classDef module fill:#4a90d9,stroke:#2c3e50,color:#fff,stroke-width:2px')
85
+ lines.push(' classDef module fill:#3b82f6,stroke:#60a5fa,color:#f8fafc,stroke-width:2px')
53
86
  for (const module of this.contract.declared.modules) {
54
87
  lines.push(` class ${this.sanitizeId(module.id)} module`)
55
88
  }
@@ -57,6 +90,75 @@ export class MainDiagramGenerator {
57
90
  return lines.join('\n')
58
91
  }
59
92
 
93
+ /**
94
+ * File-level view — for single-module or no-module projects.
95
+ * Shows the top files grouped by directory, with import edges between them.
96
+ * Caps at 25 files to keep the diagram readable.
97
+ */
98
+ private generateFileLevelView(): string {
99
+ const MAX_FILES = 25
100
+ const allFiles = Object.values(this.lock.files)
101
+
102
+ // Sort by number of connections (imports + importedBy) — most connected first
103
+ const ranked = allFiles
104
+ .map(f => ({
105
+ ...f,
106
+ connections: (f.imports?.length || 0) + (Object.values(this.lock.files).filter(other => other.imports?.includes(f.path)).length),
107
+ }))
108
+ .sort((a, b) => b.connections - a.connections)
109
+
110
+ const filesToShow = ranked.slice(0, MAX_FILES)
111
+ const shownPaths = new Set(filesToShow.map(f => f.path))
112
+ const collapsedCount = allFiles.length - filesToShow.length
113
+
114
+ const lines: string[] = []
115
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
116
+ lines.push('graph TD')
117
+ lines.push('')
118
+
119
+ // Group files by top-level directory
120
+ const byDir = new Map<string, typeof filesToShow>()
121
+ for (const file of filesToShow) {
122
+ const parts = file.path.split('/')
123
+ const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : '.'
124
+ if (!byDir.has(dir)) byDir.set(dir, [])
125
+ byDir.get(dir)!.push(file)
126
+ }
127
+
128
+ for (const [dir, files] of byDir) {
129
+ const dirLabel = dir === '.' ? 'root' : dir
130
+ lines.push(` subgraph dir_${this.sanitizeId(dir)}["📂 ${dirLabel}"]`)
131
+ for (const file of files) {
132
+ const fileName = path.basename(file.path)
133
+ const fnCount = Object.values(this.lock.functions).filter(f => f.file === file.path).length
134
+ lines.push(` ${this.sanitizeId(file.path)}["📄 ${fileName}${fnCount > 0 ? `<br/>${fnCount} fn` : ''}"]`)
135
+ }
136
+ lines.push(' end')
137
+ }
138
+
139
+ if (collapsedCount > 0) {
140
+ lines.push(` collapsed["📁 +${collapsedCount} more files"]`)
141
+ }
142
+
143
+ lines.push('')
144
+
145
+ // Add import edges between shown files
146
+ for (const file of filesToShow) {
147
+ if (!file.imports) continue
148
+ for (const imp of file.imports) {
149
+ if (shownPaths.has(imp)) {
150
+ lines.push(` ${this.sanitizeId(file.path)} --> ${this.sanitizeId(imp)}`)
151
+ }
152
+ }
153
+ }
154
+
155
+ // Style
156
+ lines.push('')
157
+ lines.push(' classDef default fill:#334155,stroke:#64748b,color:#e2e8f0,stroke-width:1px')
158
+
159
+ return lines.join('\n')
160
+ }
161
+
60
162
  private sanitizeId(id: string): string {
61
163
  return id.replace(/[^a-zA-Z0-9_]/g, '_')
62
164
  }
@@ -1,9 +1,16 @@
1
1
  import * as path from 'node:path'
2
2
  import type { MikkContract, MikkLock } from '@getmikk/core'
3
3
 
4
+ /** Maximum functions to show in a single module diagram before collapsing */
5
+ const MAX_FUNCTIONS_PER_DIAGRAM = 40
6
+
7
+ /** Maximum files to show as subgraphs before collapsing smallest ones */
8
+ const MAX_FILES_PER_DIAGRAM = 15
9
+
4
10
  /**
5
11
  * ModuleDiagramGenerator — generates a detailed Mermaid diagram for a
6
12
  * single module, showing its internal files and function call graph.
13
+ * For large modules, collapses internal-only functions to keep diagrams readable.
7
14
  * Outputs: .mikk/diagrams/modules/{moduleId}.mmd
8
15
  */
9
16
  export class ModuleDiagramGenerator {
@@ -16,23 +23,97 @@ export class ModuleDiagramGenerator {
16
23
  const module = this.contract.declared.modules.find(m => m.id === moduleId)
17
24
  if (!module) return `%% Module "${moduleId}" not found`
18
25
 
26
+ const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
27
+
28
+ // If the module is very large, only show "important" functions:
29
+ // - Exported functions (called by other modules)
30
+ // - Entry points (called by nothing internal)
31
+ // - Functions that call or are called by other modules
32
+ // - Top-N most-connected functions
33
+ let visibleFunctions = moduleFunctions
34
+ let collapsedCount = 0
35
+
36
+ if (moduleFunctions.length > MAX_FUNCTIONS_PER_DIAGRAM) {
37
+ const importantFns = new Set<string>()
38
+
39
+ for (const fn of moduleFunctions) {
40
+ // Entry points — nothing internal calls them
41
+ const isEntryPoint = fn.calledBy.length === 0
42
+ // Cross-module caller or callee
43
+ const hasCrossModuleCalls = fn.calls.some(id => {
44
+ const target = this.lock.functions[id]
45
+ return target && target.moduleId !== moduleId
46
+ })
47
+ const isCalledCrossModule = fn.calledBy.some(id => {
48
+ const caller = this.lock.functions[id]
49
+ return caller && caller.moduleId !== moduleId
50
+ })
51
+ // Exported
52
+ const isExported = fn.isExported
53
+
54
+ if (isEntryPoint || hasCrossModuleCalls || isCalledCrossModule || isExported) {
55
+ importantFns.add(fn.id)
56
+ }
57
+ }
58
+
59
+ // If still too many, pick the most connected ones
60
+ if (importantFns.size > MAX_FUNCTIONS_PER_DIAGRAM) {
61
+ const sorted = [...importantFns]
62
+ .map(id => ({ id, connections: (this.lock.functions[id]?.calls.length || 0) + (this.lock.functions[id]?.calledBy.length || 0) }))
63
+ .sort((a, b) => b.connections - a.connections)
64
+ .slice(0, MAX_FUNCTIONS_PER_DIAGRAM)
65
+ .map(x => x.id)
66
+ importantFns.clear()
67
+ for (const id of sorted) importantFns.add(id)
68
+ }
69
+
70
+ // If too few important functions, add the most connected ones
71
+ if (importantFns.size < Math.min(10, moduleFunctions.length)) {
72
+ const sorted = moduleFunctions
73
+ .filter(fn => !importantFns.has(fn.id))
74
+ .sort((a, b) => (b.calls.length + b.calledBy.length) - (a.calls.length + a.calledBy.length))
75
+ for (const fn of sorted) {
76
+ if (importantFns.size >= MAX_FUNCTIONS_PER_DIAGRAM) break
77
+ importantFns.add(fn.id)
78
+ }
79
+ }
80
+
81
+ visibleFunctions = moduleFunctions.filter(fn => importantFns.has(fn.id))
82
+ collapsedCount = moduleFunctions.length - visibleFunctions.length
83
+ }
84
+
19
85
  const lines: string[] = []
86
+ lines.push('%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "lineColor": "#64748b", "secondaryColor": "#334155", "tertiaryColor": "#475569", "background": "#0f172a", "mainBkg": "#1e293b", "nodeBorder": "#475569"}}}%%')
20
87
  lines.push(`graph TD`)
21
- lines.push(` subgraph mod_${this.sanitizeId(moduleId)}["📦 Module: ${module.name}"]`)
22
-
23
- const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
88
+ lines.push(` subgraph mod_${this.sanitizeId(moduleId)}["📦 Module: ${module.name}${collapsedCount > 0 ? ` (${collapsedCount} internal functions collapsed)` : ''}"]`)
24
89
 
25
90
  // Group functions by file
26
- const functionsByFile = new Map<string, typeof moduleFunctions>()
27
- for (const fn of moduleFunctions) {
91
+ const functionsByFile = new Map<string, typeof visibleFunctions>()
92
+ for (const fn of visibleFunctions) {
28
93
  if (!functionsByFile.has(fn.file)) {
29
94
  functionsByFile.set(fn.file, [])
30
95
  }
31
96
  functionsByFile.get(fn.file)!.push(fn)
32
97
  }
33
98
 
99
+ // If too many files, collapse the smallest ones into a summary node
100
+ let filesToRender = [...functionsByFile.entries()]
101
+ let collapsedFiles = 0
102
+ if (filesToRender.length > MAX_FILES_PER_DIAGRAM) {
103
+ // Sort by function count, keep the largest files
104
+ filesToRender.sort((a, b) => b[1].length - a[1].length)
105
+ const kept = filesToRender.slice(0, MAX_FILES_PER_DIAGRAM)
106
+ const dropped = filesToRender.slice(MAX_FILES_PER_DIAGRAM)
107
+ collapsedFiles = dropped.length
108
+ const droppedFnCount = dropped.reduce((sum, [, fns]) => sum + fns.length, 0)
109
+ filesToRender = kept
110
+
111
+ // Add a summary node for collapsed files
112
+ lines.push(` collapsed_files["📁 +${collapsedFiles} more files (${droppedFnCount} functions)"]`)
113
+ }
114
+
34
115
  // Add file subgraphs
35
- for (const [filePath, fns] of functionsByFile) {
116
+ for (const [filePath, fns] of filesToRender) {
36
117
  const fileName = path.basename(filePath)
37
118
  lines.push(` subgraph file_${this.sanitizeId(filePath)}["📄 ${fileName}"]`)
38
119
  for (const fn of fns) {
@@ -45,15 +126,16 @@ export class ModuleDiagramGenerator {
45
126
  lines.push(' end')
46
127
  lines.push('')
47
128
 
48
- // Add call edges
49
- for (const fn of moduleFunctions) {
129
+ // Add call edges (only for visible functions)
130
+ const visibleIds = new Set(visibleFunctions.map(fn => fn.id))
131
+ for (const fn of visibleFunctions) {
50
132
  for (const callTarget of fn.calls) {
51
133
  const targetFn = this.lock.functions[callTarget]
52
134
  if (targetFn) {
53
- if (targetFn.moduleId === moduleId) {
54
- // Internal call
135
+ if (targetFn.moduleId === moduleId && visibleIds.has(callTarget)) {
136
+ // Internal call (both visible)
55
137
  lines.push(` ${this.sanitizeId(fn.id)} --> ${this.sanitizeId(callTarget)}`)
56
- } else {
138
+ } else if (targetFn.moduleId !== moduleId) {
57
139
  // External call
58
140
  const targetMod = targetFn.moduleId
59
141
  lines.push(` ${this.sanitizeId(fn.id)} -.->|"calls"| ext_${this.sanitizeId(callTarget)}["🔗 ${targetFn.name}<br/>(${targetMod})"]`)
@@ -63,8 +145,8 @@ export class ModuleDiagramGenerator {
63
145
  }
64
146
 
65
147
  lines.push('')
66
- lines.push(' classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px')
67
- lines.push(' classDef external fill:#ecf0f1,stroke:#bdc3c7,stroke-dasharray: 5 5')
148
+ lines.push(' classDef default fill:#334155,stroke:#64748b,color:#e2e8f0,stroke-width:1px')
149
+ lines.push(' classDef external fill:#1e293b,stroke:#475569,color:#94a3b8,stroke-dasharray: 5 5')
68
150
 
69
151
  return lines.join('\n')
70
152
  }