@getmikk/diagram-generator 1.2.0 → 1.3.1
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 +245 -0
- package/package.json +30 -26
- package/src/generators/capsule-diagram.ts +79 -79
- package/src/generators/dependency-matrix.ts +97 -97
- package/src/generators/flow-diagram.ts +108 -108
- package/src/generators/health-diagram.ts +88 -88
- package/src/generators/impact-diagram.ts +65 -65
- package/src/generators/main-diagram.ts +63 -63
- package/src/generators/module-diagram.ts +75 -75
- package/src/index.ts +8 -8
- package/src/orchestrator.ts +76 -76
- package/tests/smoke.test.ts +5 -5
- package/tsconfig.json +15 -15
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# @getmikk/diagram-generator
|
|
2
|
+
|
|
3
|
+
> Mermaid.js diagram generation — produces architecture, flow, health, impact, capsule, and dependency matrix visualizations from your codebase's lock file.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@getmikk/diagram-generator)
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
|
|
8
|
+
`@getmikk/diagram-generator` turns the `mikk.json` contract and `mikk.lock.json` lock file into rich [Mermaid.js](https://mermaid.js.org/) diagrams. Every diagram is generated entirely from AST-derived data — no manual drawing required. Supports 7 diagram types covering everything from high-level architecture to per-function call flows.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @getmikk/diagram-generator
|
|
16
|
+
# or
|
|
17
|
+
bun add @getmikk/diagram-generator
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Peer dependency:** `@getmikk/core`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { DiagramOrchestrator } from '@getmikk/diagram-generator'
|
|
28
|
+
import { ContractReader, LockReader } from '@getmikk/core'
|
|
29
|
+
|
|
30
|
+
const contract = await new ContractReader().read('./mikk.json')
|
|
31
|
+
const lock = await new LockReader().read('./mikk.lock.json')
|
|
32
|
+
|
|
33
|
+
const orchestrator = new DiagramOrchestrator(contract, lock, process.cwd())
|
|
34
|
+
const result = await orchestrator.generateAll()
|
|
35
|
+
|
|
36
|
+
console.log(result.generated) // List of generated .mmd file paths
|
|
37
|
+
// Files written to .mikk/diagrams/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Diagram Types
|
|
43
|
+
|
|
44
|
+
### 1. Main Architecture Diagram
|
|
45
|
+
|
|
46
|
+
High-level `graph TD` showing all modules with file/function counts and inter-module edges.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { MainDiagramGenerator } from '@getmikk/diagram-generator'
|
|
50
|
+
|
|
51
|
+
const gen = new MainDiagramGenerator(contract, lock)
|
|
52
|
+
const mermaid = gen.generate()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Output example:**
|
|
56
|
+
|
|
57
|
+
```mermaid
|
|
58
|
+
graph TD
|
|
59
|
+
auth["auth<br/>📁 5 files · 📦 12 functions"]
|
|
60
|
+
payments["payments<br/>📁 3 files · 📦 8 functions"]
|
|
61
|
+
auth --> payments
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### 2. Module Detail Diagram
|
|
67
|
+
|
|
68
|
+
Zoomed-in per-module diagram showing file subgraphs, internal call edges, and external dependencies.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { ModuleDiagramGenerator } from '@getmikk/diagram-generator'
|
|
72
|
+
|
|
73
|
+
const gen = new ModuleDiagramGenerator(contract, lock)
|
|
74
|
+
const mermaid = gen.generate('auth') // module ID
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Shows:
|
|
78
|
+
- Subgraphs for each file in the module
|
|
79
|
+
- Function nodes within each file
|
|
80
|
+
- Internal call edges between functions
|
|
81
|
+
- External call links to other modules
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### 3. Impact Diagram
|
|
86
|
+
|
|
87
|
+
Visualizes the blast radius of changes — what's directly changed (red) vs. transitively impacted (orange).
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { ImpactDiagramGenerator } from '@getmikk/diagram-generator'
|
|
91
|
+
|
|
92
|
+
const gen = new ImpactDiagramGenerator(lock)
|
|
93
|
+
const mermaid = gen.generate(
|
|
94
|
+
['auth/login.ts::validateToken'], // changed node IDs
|
|
95
|
+
['payments/checkout.ts::processPayment'] // impacted node IDs
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Output:** `graph LR` with color-coded nodes:
|
|
100
|
+
- 🔴 **Red** — directly changed
|
|
101
|
+
- 🟠 **Orange** — transitively impacted
|
|
102
|
+
- Edges show the propagation chain
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### 4. Health Dashboard
|
|
107
|
+
|
|
108
|
+
Module health overview with cohesion percentage, coupling count, function count, and color-coded status.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { HealthDiagramGenerator } from '@getmikk/diagram-generator'
|
|
112
|
+
|
|
113
|
+
const gen = new HealthDiagramGenerator(contract, lock)
|
|
114
|
+
const mermaid = gen.generate()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Metrics per module:**
|
|
118
|
+
|
|
119
|
+
| Metric | Description |
|
|
120
|
+
|--------|-------------|
|
|
121
|
+
| Cohesion % | Ratio of internal calls to total calls (higher = better) |
|
|
122
|
+
| Coupling | Count of cross-module dependencies (lower = better) |
|
|
123
|
+
| Functions | Total function count |
|
|
124
|
+
| Health | 🟢 Green (>70% cohesion) · 🟡 Yellow (40-70%) · 🔴 Red (<40%) |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### 5. Flow Diagram (Sequence)
|
|
129
|
+
|
|
130
|
+
Traces a function's call chain as a Mermaid sequence diagram.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { FlowDiagramGenerator } from '@getmikk/diagram-generator'
|
|
134
|
+
|
|
135
|
+
const gen = new FlowDiagramGenerator(lock)
|
|
136
|
+
|
|
137
|
+
// Trace from a specific function
|
|
138
|
+
const sequence = gen.generate('auth/login.ts::handleLogin', /* maxDepth */ 5)
|
|
139
|
+
|
|
140
|
+
// Show all entry-point functions grouped by module
|
|
141
|
+
const entryPoints = gen.generateEntryPoints()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The sequence diagram follows the call graph depth-first, showing which function calls which, across module boundaries.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### 6. Capsule Diagram
|
|
149
|
+
|
|
150
|
+
Shows a module's public API surface — the "capsule" boundary:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { CapsuleDiagramGenerator } from '@getmikk/diagram-generator'
|
|
154
|
+
|
|
155
|
+
const gen = new CapsuleDiagramGenerator(contract, lock)
|
|
156
|
+
const mermaid = gen.generate('auth') // module ID
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Visualizes:**
|
|
160
|
+
- **Public functions** — exported and listed in the module's `publicApi`
|
|
161
|
+
- **Internal functions** — everything else
|
|
162
|
+
- **External consumers** — other modules that call into this module's public API
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### 7. Dependency Matrix
|
|
167
|
+
|
|
168
|
+
N×N cross-module dependency analysis:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { DependencyMatrixGenerator } from '@getmikk/diagram-generator'
|
|
172
|
+
|
|
173
|
+
const gen = new DependencyMatrixGenerator(contract, lock)
|
|
174
|
+
|
|
175
|
+
// Mermaid graph with weighted edges
|
|
176
|
+
const graph = gen.generate()
|
|
177
|
+
|
|
178
|
+
// Markdown table (N×N matrix)
|
|
179
|
+
const table = gen.generateTable()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Markdown table example:**
|
|
183
|
+
|
|
184
|
+
| | auth | payments | users |
|
|
185
|
+
|---|---|---|---|
|
|
186
|
+
| **auth** | — | 5 | 2 |
|
|
187
|
+
| **payments** | 1 | — | 3 |
|
|
188
|
+
| **users** | 0 | 0 | — |
|
|
189
|
+
|
|
190
|
+
Numbers represent cross-module function call counts.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## DiagramOrchestrator
|
|
195
|
+
|
|
196
|
+
The orchestrator generates all diagrams at once and writes them to `.mikk/diagrams/`:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { DiagramOrchestrator } from '@getmikk/diagram-generator'
|
|
200
|
+
|
|
201
|
+
const orchestrator = new DiagramOrchestrator(contract, lock, projectRoot)
|
|
202
|
+
|
|
203
|
+
// Generate everything
|
|
204
|
+
const result = await orchestrator.generateAll()
|
|
205
|
+
// Writes:
|
|
206
|
+
// .mikk/diagrams/main.mmd
|
|
207
|
+
// .mikk/diagrams/health.mmd
|
|
208
|
+
// .mikk/diagrams/matrix.mmd
|
|
209
|
+
// .mikk/diagrams/flow-entrypoints.mmd
|
|
210
|
+
// .mikk/diagrams/module-{id}.mmd (one per module)
|
|
211
|
+
// .mikk/diagrams/capsule-{id}.mmd (one per module)
|
|
212
|
+
|
|
213
|
+
// Generate impact diagram for specific changes
|
|
214
|
+
const impactMmd = await orchestrator.generateImpact(changedIds, impactedIds)
|
|
215
|
+
// Writes: .mikk/diagrams/impact.mmd
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Output files:** All diagrams are `.mmd` files that can be rendered with:
|
|
219
|
+
- [Mermaid Live Editor](https://mermaid.live/)
|
|
220
|
+
- GitHub Markdown (native Mermaid support)
|
|
221
|
+
- VS Code Mermaid extensions
|
|
222
|
+
- `mmdc` CLI (mermaid-cli)
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Types
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import {
|
|
230
|
+
DiagramOrchestrator,
|
|
231
|
+
MainDiagramGenerator,
|
|
232
|
+
ModuleDiagramGenerator,
|
|
233
|
+
ImpactDiagramGenerator,
|
|
234
|
+
HealthDiagramGenerator,
|
|
235
|
+
FlowDiagramGenerator,
|
|
236
|
+
CapsuleDiagramGenerator,
|
|
237
|
+
DependencyMatrixGenerator,
|
|
238
|
+
} from '@getmikk/diagram-generator'
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
[Apache-2.0](../../LICENSE)
|
package/package.json
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@getmikk/diagram-generator",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"@
|
|
25
|
-
}
|
|
26
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@getmikk/diagram-generator",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Ansh-dhanani/mikk"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"test": "bun test",
|
|
21
|
+
"dev": "tsc --watch"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@getmikk/core": "^1.3.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"@types/node": "^22.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import type { MikkContract, MikkLock } from '@getmikk/core'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CapsuleDiagramGenerator — generates capsule diagrams that show
|
|
5
|
-
* the public API surface of a module (what it exports to the world).
|
|
6
|
-
* Outputs: .mikk/diagrams/capsules/{moduleId}.mmd
|
|
7
|
-
*/
|
|
8
|
-
export class CapsuleDiagramGenerator {
|
|
9
|
-
constructor(
|
|
10
|
-
private contract: MikkContract,
|
|
11
|
-
private lock: MikkLock
|
|
12
|
-
) { }
|
|
13
|
-
|
|
14
|
-
generate(moduleId: string): string {
|
|
15
|
-
const module = this.contract.declared.modules.find(m => m.id === moduleId)
|
|
16
|
-
if (!module) return `%% Module "${moduleId}" not found`
|
|
17
|
-
|
|
18
|
-
const lines: string[] = []
|
|
19
|
-
lines.push('graph LR')
|
|
20
|
-
lines.push('')
|
|
21
|
-
|
|
22
|
-
// Module capsule (subgraph)
|
|
23
|
-
lines.push(` subgraph ${this.sanitizeId(moduleId)}["📦 ${module.name}"]`)
|
|
24
|
-
lines.push(` direction TB`)
|
|
25
|
-
|
|
26
|
-
// Find exported functions (those called by functions in other modules)
|
|
27
|
-
const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
|
|
28
|
-
const exportedFns = moduleFunctions.filter(fn =>
|
|
29
|
-
fn.calledBy.some(callerId => {
|
|
30
|
-
const caller = this.lock.functions[callerId]
|
|
31
|
-
return caller && caller.moduleId !== moduleId
|
|
32
|
-
})
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
// Internal functions
|
|
36
|
-
const internalFns = moduleFunctions.filter(fn => !exportedFns.includes(fn))
|
|
37
|
-
|
|
38
|
-
if (exportedFns.length > 0) {
|
|
39
|
-
lines.push(` subgraph public["🔓 Public API"]`)
|
|
40
|
-
for (const fn of exportedFns) {
|
|
41
|
-
lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
|
|
42
|
-
}
|
|
43
|
-
lines.push(' end')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (internalFns.length > 0 && internalFns.length <= 10) {
|
|
47
|
-
lines.push(` subgraph internal["🔒 Internal"]`)
|
|
48
|
-
for (const fn of internalFns) {
|
|
49
|
-
lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
|
|
50
|
-
}
|
|
51
|
-
lines.push(' end')
|
|
52
|
-
} else if (internalFns.length > 10) {
|
|
53
|
-
lines.push(` internal["🔒 ${internalFns.length} internal functions"]`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
lines.push(' end')
|
|
57
|
-
lines.push('')
|
|
58
|
-
|
|
59
|
-
// Show external consumers
|
|
60
|
-
for (const fn of exportedFns) {
|
|
61
|
-
for (const callerId of fn.calledBy) {
|
|
62
|
-
const caller = this.lock.functions[callerId]
|
|
63
|
-
if (caller && caller.moduleId !== moduleId) {
|
|
64
|
-
lines.push(` ext_${this.sanitizeId(callerId)}["${caller.name}<br/>(${caller.moduleId})"] --> ${this.sanitizeId(fn.id)}`)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
lines.push('')
|
|
70
|
-
lines.push(' classDef publicApi fill:#27ae60,stroke:#2c3e50,color:#fff')
|
|
71
|
-
lines.push(' classDef internalApi fill:#7f8c8d,stroke:#2c3e50,color:#fff')
|
|
72
|
-
|
|
73
|
-
return lines.join('\n')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private sanitizeId(id: string): string {
|
|
77
|
-
return id.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
import type { MikkContract, MikkLock } from '@getmikk/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CapsuleDiagramGenerator — generates capsule diagrams that show
|
|
5
|
+
* the public API surface of a module (what it exports to the world).
|
|
6
|
+
* Outputs: .mikk/diagrams/capsules/{moduleId}.mmd
|
|
7
|
+
*/
|
|
8
|
+
export class CapsuleDiagramGenerator {
|
|
9
|
+
constructor(
|
|
10
|
+
private contract: MikkContract,
|
|
11
|
+
private lock: MikkLock
|
|
12
|
+
) { }
|
|
13
|
+
|
|
14
|
+
generate(moduleId: string): string {
|
|
15
|
+
const module = this.contract.declared.modules.find(m => m.id === moduleId)
|
|
16
|
+
if (!module) return `%% Module "${moduleId}" not found`
|
|
17
|
+
|
|
18
|
+
const lines: string[] = []
|
|
19
|
+
lines.push('graph LR')
|
|
20
|
+
lines.push('')
|
|
21
|
+
|
|
22
|
+
// Module capsule (subgraph)
|
|
23
|
+
lines.push(` subgraph ${this.sanitizeId(moduleId)}["📦 ${module.name}"]`)
|
|
24
|
+
lines.push(` direction TB`)
|
|
25
|
+
|
|
26
|
+
// Find exported functions (those called by functions in other modules)
|
|
27
|
+
const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
|
|
28
|
+
const exportedFns = moduleFunctions.filter(fn =>
|
|
29
|
+
fn.calledBy.some(callerId => {
|
|
30
|
+
const caller = this.lock.functions[callerId]
|
|
31
|
+
return caller && caller.moduleId !== moduleId
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// Internal functions
|
|
36
|
+
const internalFns = moduleFunctions.filter(fn => !exportedFns.includes(fn))
|
|
37
|
+
|
|
38
|
+
if (exportedFns.length > 0) {
|
|
39
|
+
lines.push(` subgraph public["🔓 Public API"]`)
|
|
40
|
+
for (const fn of exportedFns) {
|
|
41
|
+
lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
|
|
42
|
+
}
|
|
43
|
+
lines.push(' end')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (internalFns.length > 0 && internalFns.length <= 10) {
|
|
47
|
+
lines.push(` subgraph internal["🔒 Internal"]`)
|
|
48
|
+
for (const fn of internalFns) {
|
|
49
|
+
lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
|
|
50
|
+
}
|
|
51
|
+
lines.push(' end')
|
|
52
|
+
} else if (internalFns.length > 10) {
|
|
53
|
+
lines.push(` internal["🔒 ${internalFns.length} internal functions"]`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines.push(' end')
|
|
57
|
+
lines.push('')
|
|
58
|
+
|
|
59
|
+
// Show external consumers
|
|
60
|
+
for (const fn of exportedFns) {
|
|
61
|
+
for (const callerId of fn.calledBy) {
|
|
62
|
+
const caller = this.lock.functions[callerId]
|
|
63
|
+
if (caller && caller.moduleId !== moduleId) {
|
|
64
|
+
lines.push(` ext_${this.sanitizeId(callerId)}["${caller.name}<br/>(${caller.moduleId})"] --> ${this.sanitizeId(fn.id)}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
lines.push('')
|
|
70
|
+
lines.push(' classDef publicApi fill:#27ae60,stroke:#2c3e50,color:#fff')
|
|
71
|
+
lines.push(' classDef internalApi fill:#7f8c8d,stroke:#2c3e50,color:#fff')
|
|
72
|
+
|
|
73
|
+
return lines.join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private sanitizeId(id: string): string {
|
|
77
|
+
return id.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
import type { MikkContract, MikkLock } from '@getmikk/core'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* DependencyMatrixGenerator — generates an N×N dependency matrix
|
|
5
|
-
* showing how many cross-module calls exist between each pair of modules.
|
|
6
|
-
* Useful for spotting hidden coupling between modules.
|
|
7
|
-
*
|
|
8
|
-
* Output: a Mermaid block diagram with a matrix-like layout or
|
|
9
|
-
* a structured text table that can be rendered in documentation.
|
|
10
|
-
*/
|
|
11
|
-
export class DependencyMatrixGenerator {
|
|
12
|
-
constructor(
|
|
13
|
-
private contract: MikkContract,
|
|
14
|
-
private lock: MikkLock
|
|
15
|
-
) { }
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Generate a Mermaid diagram showing the dependency matrix.
|
|
19
|
-
* Uses a graph with weighted edges between all module pairs.
|
|
20
|
-
*/
|
|
21
|
-
generate(): string {
|
|
22
|
-
const modules = this.contract.declared.modules
|
|
23
|
-
const matrix = this.computeMatrix()
|
|
24
|
-
const lines: string[] = []
|
|
25
|
-
|
|
26
|
-
lines.push('graph LR')
|
|
27
|
-
lines.push('')
|
|
28
|
-
|
|
29
|
-
// Module nodes
|
|
30
|
-
for (const mod of modules) {
|
|
31
|
-
const fnCount = Object.values(this.lock.functions)
|
|
32
|
-
.filter(f => f.moduleId === mod.id).length
|
|
33
|
-
lines.push(` ${this.sanitizeId(mod.id)}["${mod.name}<br/>${fnCount} fn"]`)
|
|
34
|
-
}
|
|
35
|
-
lines.push('')
|
|
36
|
-
|
|
37
|
-
// Weighted edges
|
|
38
|
-
for (const [key, count] of matrix) {
|
|
39
|
-
const [fromId, toId] = key.split('|')
|
|
40
|
-
if (count > 0) {
|
|
41
|
-
const thickness = count > 10 ? '==>' : count > 3 ? '-->' : '-.->'
|
|
42
|
-
lines.push(` ${this.sanitizeId(fromId)} ${thickness}|${count}| ${this.sanitizeId(toId)}`)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
lines.push('')
|
|
47
|
-
lines.push(' classDef default fill:#ecf0f1,stroke:#34495e,color:#2c3e50')
|
|
48
|
-
|
|
49
|
-
return lines.join('\n')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Generate a markdown table showing the N×N dependency matrix.
|
|
54
|
-
* Useful for claude.md or documentation.
|
|
55
|
-
*/
|
|
56
|
-
generateTable(): string {
|
|
57
|
-
const modules = this.contract.declared.modules
|
|
58
|
-
const matrix = this.computeMatrix()
|
|
59
|
-
const lines: string[] = []
|
|
60
|
-
|
|
61
|
-
// Header
|
|
62
|
-
lines.push('| From \\ To | ' + modules.map(m => m.name).join(' | ') + ' |')
|
|
63
|
-
lines.push('| --- | ' + modules.map(() => '---').join(' | ') + ' |')
|
|
64
|
-
|
|
65
|
-
// Rows
|
|
66
|
-
for (const fromMod of modules) {
|
|
67
|
-
const cells = modules.map(toMod => {
|
|
68
|
-
if (fromMod.id === toMod.id) return '-'
|
|
69
|
-
const count = matrix.get(`${fromMod.id}|${toMod.id}`) || 0
|
|
70
|
-
return count > 0 ? String(count) : '0'
|
|
71
|
-
})
|
|
72
|
-
lines.push(`| **${fromMod.name}** | ${cells.join(' | ')} |`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return lines.join('\n')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private computeMatrix(): Map<string, number> {
|
|
79
|
-
const counts = new Map<string, number>()
|
|
80
|
-
|
|
81
|
-
for (const fn of Object.values(this.lock.functions)) {
|
|
82
|
-
for (const callTarget of fn.calls) {
|
|
83
|
-
const targetFn = this.lock.functions[callTarget]
|
|
84
|
-
if (targetFn && fn.moduleId !== targetFn.moduleId) {
|
|
85
|
-
const key = `${fn.moduleId}|${targetFn.moduleId}`
|
|
86
|
-
counts.set(key, (counts.get(key) || 0) + 1)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return counts
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private sanitizeId(id: string): string {
|
|
95
|
-
return id.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
96
|
-
}
|
|
97
|
-
}
|
|
1
|
+
import type { MikkContract, MikkLock } from '@getmikk/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DependencyMatrixGenerator — generates an N×N dependency matrix
|
|
5
|
+
* showing how many cross-module calls exist between each pair of modules.
|
|
6
|
+
* Useful for spotting hidden coupling between modules.
|
|
7
|
+
*
|
|
8
|
+
* Output: a Mermaid block diagram with a matrix-like layout or
|
|
9
|
+
* a structured text table that can be rendered in documentation.
|
|
10
|
+
*/
|
|
11
|
+
export class DependencyMatrixGenerator {
|
|
12
|
+
constructor(
|
|
13
|
+
private contract: MikkContract,
|
|
14
|
+
private lock: MikkLock
|
|
15
|
+
) { }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a Mermaid diagram showing the dependency matrix.
|
|
19
|
+
* Uses a graph with weighted edges between all module pairs.
|
|
20
|
+
*/
|
|
21
|
+
generate(): string {
|
|
22
|
+
const modules = this.contract.declared.modules
|
|
23
|
+
const matrix = this.computeMatrix()
|
|
24
|
+
const lines: string[] = []
|
|
25
|
+
|
|
26
|
+
lines.push('graph LR')
|
|
27
|
+
lines.push('')
|
|
28
|
+
|
|
29
|
+
// Module nodes
|
|
30
|
+
for (const mod of modules) {
|
|
31
|
+
const fnCount = Object.values(this.lock.functions)
|
|
32
|
+
.filter(f => f.moduleId === mod.id).length
|
|
33
|
+
lines.push(` ${this.sanitizeId(mod.id)}["${mod.name}<br/>${fnCount} fn"]`)
|
|
34
|
+
}
|
|
35
|
+
lines.push('')
|
|
36
|
+
|
|
37
|
+
// Weighted edges
|
|
38
|
+
for (const [key, count] of matrix) {
|
|
39
|
+
const [fromId, toId] = key.split('|')
|
|
40
|
+
if (count > 0) {
|
|
41
|
+
const thickness = count > 10 ? '==>' : count > 3 ? '-->' : '-.->'
|
|
42
|
+
lines.push(` ${this.sanitizeId(fromId)} ${thickness}|${count}| ${this.sanitizeId(toId)}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
lines.push('')
|
|
47
|
+
lines.push(' classDef default fill:#ecf0f1,stroke:#34495e,color:#2c3e50')
|
|
48
|
+
|
|
49
|
+
return lines.join('\n')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a markdown table showing the N×N dependency matrix.
|
|
54
|
+
* Useful for claude.md or documentation.
|
|
55
|
+
*/
|
|
56
|
+
generateTable(): string {
|
|
57
|
+
const modules = this.contract.declared.modules
|
|
58
|
+
const matrix = this.computeMatrix()
|
|
59
|
+
const lines: string[] = []
|
|
60
|
+
|
|
61
|
+
// Header
|
|
62
|
+
lines.push('| From \\ To | ' + modules.map(m => m.name).join(' | ') + ' |')
|
|
63
|
+
lines.push('| --- | ' + modules.map(() => '---').join(' | ') + ' |')
|
|
64
|
+
|
|
65
|
+
// Rows
|
|
66
|
+
for (const fromMod of modules) {
|
|
67
|
+
const cells = modules.map(toMod => {
|
|
68
|
+
if (fromMod.id === toMod.id) return '-'
|
|
69
|
+
const count = matrix.get(`${fromMod.id}|${toMod.id}`) || 0
|
|
70
|
+
return count > 0 ? String(count) : '0'
|
|
71
|
+
})
|
|
72
|
+
lines.push(`| **${fromMod.name}** | ${cells.join(' | ')} |`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join('\n')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private computeMatrix(): Map<string, number> {
|
|
79
|
+
const counts = new Map<string, number>()
|
|
80
|
+
|
|
81
|
+
for (const fn of Object.values(this.lock.functions)) {
|
|
82
|
+
for (const callTarget of fn.calls) {
|
|
83
|
+
const targetFn = this.lock.functions[callTarget]
|
|
84
|
+
if (targetFn && fn.moduleId !== targetFn.moduleId) {
|
|
85
|
+
const key = `${fn.moduleId}|${targetFn.moduleId}`
|
|
86
|
+
counts.set(key, (counts.get(key) || 0) + 1)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return counts
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private sanitizeId(id: string): string {
|
|
95
|
+
return id.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
96
|
+
}
|
|
97
|
+
}
|