@getmikk/core 1.5.1 → 1.7.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 +44 -6
- package/out.log +0 -0
- package/package.json +4 -3
- package/src/contract/adr-manager.ts +75 -0
- package/src/contract/index.ts +2 -0
- package/src/contract/lock-compiler.ts +6 -5
- package/src/contract/lock-reader.ts +40 -26
- package/src/contract/schema.ts +2 -7
- package/src/graph/dead-code-detector.ts +194 -0
- package/src/graph/graph-builder.ts +6 -2
- package/src/graph/impact-analyzer.ts +53 -2
- package/src/graph/index.ts +4 -1
- package/src/graph/types.ts +21 -0
- package/src/index.ts +1 -1
- package/src/parser/go/go-extractor.ts +712 -0
- package/src/parser/go/go-parser.ts +41 -0
- package/src/parser/go/go-resolver.ts +70 -0
- package/src/parser/index.ts +46 -6
- package/src/parser/javascript/js-extractor.ts +262 -0
- package/src/parser/javascript/js-parser.ts +92 -0
- package/src/parser/javascript/js-resolver.ts +83 -0
- package/src/parser/types.ts +1 -1
- package/src/parser/typescript/ts-extractor.ts +93 -42
- package/src/parser/typescript/ts-parser.ts +120 -1
- package/test-output.txt +0 -0
- package/tests/adr-manager.test.ts +97 -0
- package/tests/contract.test.ts +1 -1
- package/tests/dead-code.test.ts +134 -0
- package/tests/go-parser.test.ts +366 -0
- package/tests/helpers.ts +0 -1
- package/tests/impact-classified.test.ts +78 -0
- package/tests/js-parser.test.ts +616 -0
- package/tests/ts-parser.test.ts +93 -0
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# @getmikk/core
|
|
2
2
|
|
|
3
|
-
> AST parsing, dependency graph construction, Merkle-tree hashing, contract management, and
|
|
3
|
+
> The foundation of the Mikk ecosystem — TypeScript AST parsing, dependency graph construction, Merkle-tree hashing, contract management, and lock file compilation.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@getmikk/core)
|
|
6
6
|
[](../../LICENSE)
|
|
7
7
|
|
|
8
|
-
`@getmikk/core` is the foundation
|
|
8
|
+
`@getmikk/core` is the foundation every other Mikk package builds on. It owns the complete pipeline for turning raw **TypeScript and Go** source into structured, queryable intelligence: parsing source files into real ASTs (TS Compiler API for TypeScript; regex + stateful scanning for Go; no external toolchain required), building a two-pass dependency graph with O(1) adjacency lookups, computing Merkle-tree SHA-256 hashes at function → file → module → root level, and compiling everything into a `mikk.lock.json` snapshot that every other package reads from.
|
|
9
|
+
|
|
10
|
+
Every AI context query, impact analysis, contract validation, and diagram generation ultimately runs on the graph and lock file produced here.
|
|
11
|
+
|
|
12
|
+
> Part of [Mikk](../../README.md) — the codebase nervous system for AI-assisted development.
|
|
9
13
|
|
|
10
14
|
---
|
|
11
15
|
|
|
@@ -22,11 +26,11 @@ bun add @getmikk/core
|
|
|
22
26
|
## Architecture Overview
|
|
23
27
|
|
|
24
28
|
```
|
|
25
|
-
Source Files (.ts/.tsx)
|
|
29
|
+
Source Files (.ts/.tsx/.go)
|
|
26
30
|
│
|
|
27
31
|
▼
|
|
28
32
|
┌─────────┐
|
|
29
|
-
│ Parser │ ← TypeScriptParser
|
|
33
|
+
│ Parser │ ← TypeScriptParser / GoParser
|
|
30
34
|
└────┬────┘
|
|
31
35
|
│ ParsedFile[]
|
|
32
36
|
▼
|
|
@@ -96,6 +100,39 @@ const resolver = new TypeScriptResolver()
|
|
|
96
100
|
const resolved = resolver.resolve(importDecl, fromFilePath, allProjectFiles)
|
|
97
101
|
```
|
|
98
102
|
|
|
103
|
+
#### Go Parser
|
|
104
|
+
|
|
105
|
+
Parses `.go` files without requiring the Go toolchain:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { GoParser } from '@getmikk/core'
|
|
109
|
+
|
|
110
|
+
const parser = new GoParser()
|
|
111
|
+
const parsed = parser.parse('service.go', fileContent)
|
|
112
|
+
|
|
113
|
+
console.log(parsed.functions) // ParsedFunction[] — name, params with types, return type, calls[]
|
|
114
|
+
console.log(parsed.classes) // ParsedClass[] — receiver-based methods grouped by type name
|
|
115
|
+
console.log(parsed.imports) // ParsedImport[] — resolved against go.mod module path
|
|
116
|
+
console.log(parsed.exports) // ParsedExport[] — uppercase identifiers (Go convention)
|
|
117
|
+
console.log(parsed.routes) // ParsedRoute[] — Gin, Echo, Chi, Mux, net/http routes
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Features**:
|
|
121
|
+
- Stateful line/character scanning (handles strings, comments, nested braces correctly)
|
|
122
|
+
- Receiver methods grouped with struct types as classes
|
|
123
|
+
- HTTP route detection (Gin/Echo/Chi/Mux/net.http/Fiber patterns)
|
|
124
|
+
- Error handling detection (`if err != nil` patterns)
|
|
125
|
+
- Function call extraction from bodies
|
|
126
|
+
- Grouped parameter expansion (`first, last string` → both typed as `string`)
|
|
127
|
+
- Import resolution via `go.mod` module path
|
|
128
|
+
|
|
129
|
+
#### Auto-detection by file extension
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const parser = getParser('file.ts') // → TypeScriptParser
|
|
133
|
+
const parser = getParser('service.go') // → GoParser
|
|
134
|
+
```
|
|
135
|
+
|
|
99
136
|
---
|
|
100
137
|
|
|
101
138
|
### 2. Graph — Dependency Graph Construction
|
|
@@ -135,7 +172,8 @@ const impact = analyzer.analyze(['src/utils/math.ts::calculateTotal'])
|
|
|
135
172
|
console.log(impact.changed) // string[] — directly changed node IDs
|
|
136
173
|
console.log(impact.impacted) // string[] — transitively affected nodes
|
|
137
174
|
console.log(impact.depth) // number — max propagation depth
|
|
138
|
-
console.log(impact.confidence) //
|
|
175
|
+
console.log(impact.confidence) // 'high' | 'medium' | 'low'
|
|
176
|
+
console.log(impact.classified) // { critical: [], high: [], medium: [], low: [] }
|
|
139
177
|
```
|
|
140
178
|
|
|
141
179
|
#### ClusterDetector
|
|
@@ -414,7 +452,7 @@ import type {
|
|
|
414
452
|
// Parser
|
|
415
453
|
ParsedFile, ParsedFunction, ParsedClass, ParsedImport, ParsedExport, ParsedParam, ParsedGeneric,
|
|
416
454
|
// Graph
|
|
417
|
-
DependencyGraph, GraphNode, GraphEdge, ImpactResult, NodeType, EdgeType, ModuleCluster,
|
|
455
|
+
DependencyGraph, GraphNode, GraphEdge, ImpactResult, NodeType, EdgeType, ModuleCluster, ClassifiedImpact, RiskLevel,
|
|
418
456
|
// Contract
|
|
419
457
|
MikkContract, MikkLock, MikkModule, MikkDecision, MikkLockFunction, MikkLockModule, MikkLockFile,
|
|
420
458
|
// Boundary
|
package/out.log
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikk/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"zod": "^3.22.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"
|
|
31
|
-
"@types/node": "^22.0.0"
|
|
30
|
+
"@types/bun": "^1.3.10",
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"typescript": "^5.7.0"
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
|
+
import type { MikkContract, MikkDecision } from './schema.js'
|
|
3
|
+
import { MikkContractSchema } from './schema.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AdrManager — CRUD operations on Architectural Decision Records
|
|
7
|
+
* stored in the `declared.decisions` array of mikk.json.
|
|
8
|
+
*
|
|
9
|
+
* Each ADR has: id, title, reason, date.
|
|
10
|
+
* Exposed as an MCP tool so AI assistants can read and update ADRs.
|
|
11
|
+
*/
|
|
12
|
+
export class AdrManager {
|
|
13
|
+
constructor(private contractPath: string) { }
|
|
14
|
+
|
|
15
|
+
// ─── Read ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
async list(): Promise<MikkDecision[]> {
|
|
18
|
+
const contract = await this.readContract()
|
|
19
|
+
return contract.declared.decisions ?? []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get(id: string): Promise<MikkDecision | null> {
|
|
23
|
+
const decisions = await this.list()
|
|
24
|
+
return decisions.find(d => d.id === id) ?? null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Write ─────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
async add(decision: MikkDecision): Promise<void> {
|
|
30
|
+
const contract = await this.readContract()
|
|
31
|
+
if (!contract.declared.decisions) {
|
|
32
|
+
contract.declared.decisions = []
|
|
33
|
+
}
|
|
34
|
+
// Check for duplicate ID
|
|
35
|
+
if (contract.declared.decisions.some(d => d.id === decision.id)) {
|
|
36
|
+
throw new Error(`ADR with id "${decision.id}" already exists. Use update() instead.`)
|
|
37
|
+
}
|
|
38
|
+
contract.declared.decisions.push(decision)
|
|
39
|
+
await this.writeContract(contract)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async update(id: string, fields: Partial<Omit<MikkDecision, 'id'>>): Promise<void> {
|
|
43
|
+
const contract = await this.readContract()
|
|
44
|
+
const decisions = contract.declared.decisions ?? []
|
|
45
|
+
const idx = decisions.findIndex(d => d.id === id)
|
|
46
|
+
if (idx === -1) {
|
|
47
|
+
throw new Error(`ADR "${id}" not found. Use add() to create a new decision.`)
|
|
48
|
+
}
|
|
49
|
+
decisions[idx] = { ...decisions[idx], ...fields, id } // preserve id
|
|
50
|
+
contract.declared.decisions = decisions
|
|
51
|
+
await this.writeContract(contract)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async remove(id: string): Promise<boolean> {
|
|
55
|
+
const contract = await this.readContract()
|
|
56
|
+
const decisions = contract.declared.decisions ?? []
|
|
57
|
+
const idx = decisions.findIndex(d => d.id === id)
|
|
58
|
+
if (idx === -1) return false
|
|
59
|
+
decisions.splice(idx, 1)
|
|
60
|
+
contract.declared.decisions = decisions
|
|
61
|
+
await this.writeContract(contract)
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Helpers ───────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
private async readContract(): Promise<MikkContract> {
|
|
68
|
+
const raw = await fs.readFile(this.contractPath, 'utf-8')
|
|
69
|
+
return MikkContractSchema.parse(JSON.parse(raw))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async writeContract(contract: MikkContract): Promise<void> {
|
|
73
|
+
await fs.writeFile(this.contractPath, JSON.stringify(contract, null, 2), 'utf-8')
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/contract/index.ts
CHANGED
|
@@ -10,3 +10,5 @@ export { ContractWriter, type UpdateResult } from './contract-writer.js'
|
|
|
10
10
|
export { ContractReader } from './contract-reader.js'
|
|
11
11
|
export { LockReader } from './lock-reader.js'
|
|
12
12
|
export { ContractGenerator } from './contract-generator.js'
|
|
13
|
+
export { AdrManager } from './adr-manager.js'
|
|
14
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
1
|
+
import * as path from 'node:path'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import type { MikkContract, MikkLock } from './schema.js'
|
|
4
4
|
import type { DependencyGraph } from '../graph/types.js'
|
|
@@ -131,7 +131,7 @@ export class LockCompiler {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const lockData: MikkLock = {
|
|
134
|
-
version: '1.
|
|
134
|
+
version: '1.7.0',
|
|
135
135
|
generatedAt: new Date().toISOString(),
|
|
136
136
|
generatorVersion: VERSION,
|
|
137
137
|
projectRoot: contract.project.name,
|
|
@@ -146,7 +146,9 @@ export class LockCompiler {
|
|
|
146
146
|
classes: Object.keys(classes).length > 0 ? classes : undefined,
|
|
147
147
|
generics: Object.keys(generics).length > 0 ? generics : undefined,
|
|
148
148
|
files,
|
|
149
|
-
contextFiles: contextFiles && contextFiles.length > 0
|
|
149
|
+
contextFiles: contextFiles && contextFiles.length > 0
|
|
150
|
+
? contextFiles.map(({ path, type, size }) => ({ path, type, size }))
|
|
151
|
+
: undefined,
|
|
150
152
|
routes: routes.length > 0 ? routes : undefined,
|
|
151
153
|
graph: {
|
|
152
154
|
nodes: graph.nodes.size,
|
|
@@ -205,7 +207,6 @@ export class LockCompiler {
|
|
|
205
207
|
),
|
|
206
208
|
edgeCasesHandled: node.metadata.edgeCasesHandled,
|
|
207
209
|
errorHandling: node.metadata.errorHandling,
|
|
208
|
-
detailedLines: node.metadata.detailedLines,
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -333,7 +334,7 @@ export class LockCompiler {
|
|
|
333
334
|
path: file.path,
|
|
334
335
|
hash: file.hash,
|
|
335
336
|
moduleId: moduleId || 'unknown',
|
|
336
|
-
lastModified: new Date().toISOString(),
|
|
337
|
+
lastModified: new Date(file.parsedAt).toISOString(),
|
|
337
338
|
...(importedFiles.length > 0 ? { imports: importedFiles } : {}),
|
|
338
339
|
}
|
|
339
340
|
}
|
|
@@ -32,7 +32,7 @@ export class LockReader {
|
|
|
32
32
|
/** Write lock file to disk in compact format */
|
|
33
33
|
async write(lock: MikkLock, lockPath: string): Promise<void> {
|
|
34
34
|
const compact = compactifyLock(lock)
|
|
35
|
-
const json = JSON.stringify(compact
|
|
35
|
+
const json = JSON.stringify(compact)
|
|
36
36
|
await fs.writeFile(lockPath, json, 'utf-8')
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -59,17 +59,23 @@ function compactifyLock(lock: MikkLock): any {
|
|
|
59
59
|
graph: lock.graph,
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// P7: Build fnIndex for integer edge references
|
|
63
|
+
const fnKeys = Object.keys(lock.functions)
|
|
64
|
+
const fnIndexMap = new Map<string, number>()
|
|
65
|
+
fnKeys.forEach((k, i) => fnIndexMap.set(k, i))
|
|
66
|
+
out.fnIndex = fnKeys
|
|
67
|
+
|
|
62
68
|
// Functions — biggest savings
|
|
63
69
|
out.functions = {}
|
|
64
|
-
for (
|
|
70
|
+
for (let idx = 0; idx < fnKeys.length; idx++) {
|
|
71
|
+
const fn = lock.functions[fnKeys[idx]]
|
|
65
72
|
const c: any = {
|
|
66
73
|
lines: [fn.startLine, fn.endLine],
|
|
67
|
-
|
|
74
|
+
// P4: no hash, P6: no moduleId
|
|
68
75
|
}
|
|
69
|
-
//
|
|
70
|
-
if (fn.
|
|
71
|
-
if (fn.
|
|
72
|
-
if (fn.calledBy.length > 0) c.calledBy = fn.calledBy
|
|
76
|
+
// P7: integer calls/calledBy referencing fnIndex positions
|
|
77
|
+
if (fn.calls.length > 0) c.calls = fn.calls.map(id => fnIndexMap.get(id) ?? -1).filter((n: number) => n >= 0)
|
|
78
|
+
if (fn.calledBy.length > 0) c.calledBy = fn.calledBy.map(id => fnIndexMap.get(id) ?? -1).filter((n: number) => n >= 0)
|
|
73
79
|
if (fn.params && fn.params.length > 0) c.params = fn.params
|
|
74
80
|
if (fn.returnType) c.returnType = fn.returnType
|
|
75
81
|
if (fn.isAsync) c.isAsync = true
|
|
@@ -79,10 +85,8 @@ function compactifyLock(lock: MikkLock): any {
|
|
|
79
85
|
if (fn.errorHandling && fn.errorHandling.length > 0) {
|
|
80
86
|
c.errors = fn.errorHandling.map(e => [e.line, e.type, e.detail])
|
|
81
87
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
out.functions[key] = c
|
|
88
|
+
// P2: no c.details (detailedLines removed)
|
|
89
|
+
out.functions[String(idx)] = c
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
// Classes
|
|
@@ -134,9 +138,9 @@ function compactifyLock(lock: MikkLock): any {
|
|
|
134
138
|
out.files[key] = c
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
// Context files —
|
|
141
|
+
// Context files — paths/type only, no content
|
|
138
142
|
if (lock.contextFiles && lock.contextFiles.length > 0) {
|
|
139
|
-
out.contextFiles = lock.contextFiles
|
|
143
|
+
out.contextFiles = lock.contextFiles.map(({ path, type, size }) => ({ path, type, size }))
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
// Routes — keep as-is (already compact)
|
|
@@ -166,23 +170,37 @@ function hydrateLock(raw: any): any {
|
|
|
166
170
|
graph: raw.graph,
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// P7: function index for integer edge resolution
|
|
174
|
+
const fnIndex: string[] = raw.fnIndex || []
|
|
175
|
+
const hasFnIndex = fnIndex.length > 0
|
|
176
|
+
|
|
177
|
+
// P6: build file→moduleId map before function loop
|
|
178
|
+
const fileModuleMap: Record<string, string> = {}
|
|
179
|
+
for (const [key, c] of Object.entries(raw.files || {}) as [string, any][]) {
|
|
180
|
+
fileModuleMap[key] = c.moduleId || 'unknown'
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
// Hydrate functions
|
|
170
184
|
out.functions = {}
|
|
171
185
|
for (const [key, c] of Object.entries(raw.functions || {}) as [string, any][]) {
|
|
172
|
-
//
|
|
173
|
-
const
|
|
186
|
+
// P7: key is integer index → look up full ID via fnIndex
|
|
187
|
+
const fullId = hasFnIndex ? (fnIndex[parseInt(key)] || key) : key
|
|
188
|
+
const { name, file } = parseEntityKey(fullId, 'fn:')
|
|
174
189
|
const lines = c.lines || [c.startLine || 0, c.endLine || 0]
|
|
190
|
+
// P7: integer calls/calledBy → resolve to full string IDs (backward compat: strings pass through)
|
|
191
|
+
const calls = (c.calls || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
|
|
192
|
+
const calledBy = (c.calledBy || []).map((v: any) => typeof v === 'number' ? (fnIndex[v] ?? null) : v).filter(Boolean)
|
|
175
193
|
|
|
176
|
-
out.functions[
|
|
177
|
-
id:
|
|
194
|
+
out.functions[fullId] = {
|
|
195
|
+
id: fullId,
|
|
178
196
|
name,
|
|
179
197
|
file,
|
|
180
198
|
startLine: lines[0],
|
|
181
199
|
endLine: lines[1],
|
|
182
|
-
hash: c.hash || '',
|
|
183
|
-
calls
|
|
184
|
-
calledBy
|
|
185
|
-
moduleId: c.moduleId || 'unknown',
|
|
200
|
+
hash: c.hash || '', // P4: empty string when not stored
|
|
201
|
+
calls,
|
|
202
|
+
calledBy,
|
|
203
|
+
moduleId: fileModuleMap[file] || c.moduleId || 'unknown', // P6: derive from file
|
|
186
204
|
...(c.params ? { params: c.params } : {}),
|
|
187
205
|
...(c.returnType ? { returnType: c.returnType } : {}),
|
|
188
206
|
...(c.isAsync ? { isAsync: true } : {}),
|
|
@@ -194,11 +212,7 @@ function hydrateLock(raw: any): any {
|
|
|
194
212
|
line: e[0], type: e[1], detail: e[2]
|
|
195
213
|
}))
|
|
196
214
|
} : {}),
|
|
197
|
-
|
|
198
|
-
detailedLines: c.details.map((d: any) => ({
|
|
199
|
-
startLine: d[0], endLine: d[1], blockType: d[2]
|
|
200
|
-
}))
|
|
201
|
-
} : {}),
|
|
215
|
+
// P2: no detailedLines restoration
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
218
|
|
package/src/contract/schema.ts
CHANGED
|
@@ -74,11 +74,6 @@ export const MikkLockFunctionSchema = z.object({
|
|
|
74
74
|
type: z.enum(['try-catch', 'throw']),
|
|
75
75
|
detail: z.string(),
|
|
76
76
|
})).optional(),
|
|
77
|
-
detailedLines: z.array(z.object({
|
|
78
|
-
startLine: z.number(),
|
|
79
|
-
endLine: z.number(),
|
|
80
|
-
blockType: z.string(),
|
|
81
|
-
})).optional()
|
|
82
77
|
})
|
|
83
78
|
|
|
84
79
|
export const MikkLockModuleSchema = z.object({
|
|
@@ -129,9 +124,9 @@ export const MikkLockGenericSchema = z.object({
|
|
|
129
124
|
|
|
130
125
|
export const MikkLockContextFileSchema = z.object({
|
|
131
126
|
path: z.string(),
|
|
132
|
-
content: z.string(),
|
|
127
|
+
content: z.string().optional(),
|
|
133
128
|
type: z.enum(['schema', 'model', 'types', 'routes', 'config', 'api-spec', 'migration', 'docker']),
|
|
134
|
-
size: z.number(),
|
|
129
|
+
size: z.number().optional(),
|
|
135
130
|
})
|
|
136
131
|
|
|
137
132
|
export const MikkLockRouteSchema = z.object({
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { DependencyGraph } from './types.js'
|
|
2
|
+
import type { MikkLock } from '../contract/schema.js'
|
|
3
|
+
|
|
4
|
+
// ─── Types ──────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface DeadCodeEntry {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
file: string
|
|
10
|
+
moduleId?: string
|
|
11
|
+
type: 'function' | 'class'
|
|
12
|
+
reason: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DeadCodeResult {
|
|
16
|
+
deadFunctions: DeadCodeEntry[]
|
|
17
|
+
totalFunctions: number
|
|
18
|
+
deadCount: number
|
|
19
|
+
deadPercentage: number
|
|
20
|
+
byModule: Record<string, { dead: number; total: number; items: DeadCodeEntry[] }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Exemption patterns ────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Common entry-point function names that are never "dead" even with 0 callers */
|
|
26
|
+
const ENTRY_POINT_PATTERNS = [
|
|
27
|
+
/^(main|bootstrap|start|init|setup|configure|register|mount)$/i,
|
|
28
|
+
/^(app|server|index|mod|program)$/i,
|
|
29
|
+
/Handler$/i, // Express/Koa/Hono handlers
|
|
30
|
+
/Middleware$/i,
|
|
31
|
+
/Controller$/i,
|
|
32
|
+
/^use[A-Z]/, // React hooks
|
|
33
|
+
/^handle[A-Z]/, // Event handlers
|
|
34
|
+
/^on[A-Z]/, // Event listeners
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/** Common test function patterns */
|
|
38
|
+
const TEST_PATTERNS = [
|
|
39
|
+
/^(it|describe|test|beforeAll|afterAll|beforeEach|afterEach)$/,
|
|
40
|
+
/\.test\./,
|
|
41
|
+
/\.spec\./,
|
|
42
|
+
/__test__/,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// ─── Detector ──────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* DeadCodeDetector — walks the dependency graph and finds functions
|
|
49
|
+
* with zero incoming `calls` edges after applying multi-pass exemptions.
|
|
50
|
+
*
|
|
51
|
+
* Exemptions:
|
|
52
|
+
* 1. Exported symbols (may be consumed externally)
|
|
53
|
+
* 2. Entry point patterns (main, handler, middleware, hooks, etc.)
|
|
54
|
+
* 3. Route handlers (detected HTTP routes)
|
|
55
|
+
* 4. Test functions (describe, it, test, etc.)
|
|
56
|
+
* 5. Decorated classes/functions (typically framework-managed)
|
|
57
|
+
* 6. Constructor methods (called implicitly)
|
|
58
|
+
*/
|
|
59
|
+
export class DeadCodeDetector {
|
|
60
|
+
private routeHandlers: Set<string>
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private graph: DependencyGraph,
|
|
64
|
+
private lock: MikkLock,
|
|
65
|
+
) {
|
|
66
|
+
// Build a set of handler function names from detected routes
|
|
67
|
+
this.routeHandlers = new Set(
|
|
68
|
+
(lock.routes ?? []).map(r => r.handler).filter(Boolean),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
detect(): DeadCodeResult {
|
|
73
|
+
const dead: DeadCodeEntry[] = []
|
|
74
|
+
let totalFunctions = 0
|
|
75
|
+
const byModule: DeadCodeResult['byModule'] = {}
|
|
76
|
+
|
|
77
|
+
for (const [id, fn] of Object.entries(this.lock.functions)) {
|
|
78
|
+
totalFunctions++
|
|
79
|
+
const moduleId = fn.moduleId ?? 'unknown'
|
|
80
|
+
|
|
81
|
+
// Initialize module bucket
|
|
82
|
+
if (!byModule[moduleId]) {
|
|
83
|
+
byModule[moduleId] = { dead: 0, total: 0, items: [] }
|
|
84
|
+
}
|
|
85
|
+
byModule[moduleId].total++
|
|
86
|
+
|
|
87
|
+
// Check if this function has any incoming call edges
|
|
88
|
+
const inEdges = this.graph.inEdges.get(id) || []
|
|
89
|
+
const hasCallers = inEdges.some(e => e.type === 'calls')
|
|
90
|
+
|
|
91
|
+
if (hasCallers) continue // Not dead
|
|
92
|
+
|
|
93
|
+
// Apply exemptions
|
|
94
|
+
if (this.isExempt(fn, id)) continue
|
|
95
|
+
|
|
96
|
+
const entry: DeadCodeEntry = {
|
|
97
|
+
id,
|
|
98
|
+
name: fn.name,
|
|
99
|
+
file: fn.file,
|
|
100
|
+
moduleId,
|
|
101
|
+
type: 'function',
|
|
102
|
+
reason: this.inferReason(fn, id),
|
|
103
|
+
}
|
|
104
|
+
dead.push(entry)
|
|
105
|
+
byModule[moduleId].dead++
|
|
106
|
+
byModule[moduleId].items.push(entry)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Also check classes (if present in lock)
|
|
110
|
+
if (this.lock.classes) {
|
|
111
|
+
for (const [id, cls] of Object.entries(this.lock.classes)) {
|
|
112
|
+
const moduleId = cls.moduleId ?? 'unknown'
|
|
113
|
+
if (!byModule[moduleId]) {
|
|
114
|
+
byModule[moduleId] = { dead: 0, total: 0, items: [] }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const inEdges = this.graph.inEdges.get(id) || []
|
|
118
|
+
const hasCallers = inEdges.some(e => e.type === 'calls' || e.type === 'imports')
|
|
119
|
+
|
|
120
|
+
if (hasCallers) continue
|
|
121
|
+
if (cls.isExported) continue // Exported classes are exempt
|
|
122
|
+
|
|
123
|
+
const entry: DeadCodeEntry = {
|
|
124
|
+
id,
|
|
125
|
+
name: cls.name,
|
|
126
|
+
file: cls.file,
|
|
127
|
+
moduleId,
|
|
128
|
+
type: 'class',
|
|
129
|
+
reason: 'Class has no callers or importers and is not exported',
|
|
130
|
+
}
|
|
131
|
+
dead.push(entry)
|
|
132
|
+
byModule[moduleId].dead++
|
|
133
|
+
byModule[moduleId].items.push(entry)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
deadFunctions: dead,
|
|
139
|
+
totalFunctions,
|
|
140
|
+
deadCount: dead.length,
|
|
141
|
+
deadPercentage: totalFunctions > 0
|
|
142
|
+
? Math.round((dead.length / totalFunctions) * 1000) / 10
|
|
143
|
+
: 0,
|
|
144
|
+
byModule,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Exemption checks ──────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
private isExempt(fn: MikkLock['functions'][string], id: string): boolean {
|
|
151
|
+
// 1. Exported functions — may be consumed by external packages
|
|
152
|
+
if (fn.isExported) return true
|
|
153
|
+
|
|
154
|
+
// 2. Entry point patterns
|
|
155
|
+
if (ENTRY_POINT_PATTERNS.some(p => p.test(fn.name))) return true
|
|
156
|
+
|
|
157
|
+
// 3. Route handlers
|
|
158
|
+
if (this.routeHandlers.has(fn.name)) return true
|
|
159
|
+
|
|
160
|
+
// 4. Test functions or in test files
|
|
161
|
+
if (TEST_PATTERNS.some(p => p.test(fn.name) || p.test(fn.file))) return true
|
|
162
|
+
|
|
163
|
+
// 5. Constructor methods
|
|
164
|
+
if (fn.name === 'constructor' || fn.name === '__init__') return true
|
|
165
|
+
|
|
166
|
+
// 6. Functions called by exported functions in the same file
|
|
167
|
+
// (transitive liveness — if an exported fn calls this, it's alive)
|
|
168
|
+
if (this.isCalledByExportedInSameFile(fn, id)) return true
|
|
169
|
+
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private isCalledByExportedInSameFile(
|
|
174
|
+
fn: MikkLock['functions'][string],
|
|
175
|
+
fnId: string,
|
|
176
|
+
): boolean {
|
|
177
|
+
// Check calledBy — if any caller is exported and in the same file, exempt
|
|
178
|
+
for (const callerId of fn.calledBy) {
|
|
179
|
+
const caller = this.lock.functions[callerId]
|
|
180
|
+
if (caller && caller.isExported && caller.file === fn.file) {
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private inferReason(fn: MikkLock['functions'][string], id: string): string {
|
|
188
|
+
if (fn.calledBy.length === 0) {
|
|
189
|
+
return 'No callers found anywhere in the codebase'
|
|
190
|
+
}
|
|
191
|
+
// calledBy has entries but they didn't resolve to graph edges
|
|
192
|
+
return `${fn.calledBy.length} references exist but none resolved to active call edges`
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -121,6 +121,7 @@ export class GraphBuilder {
|
|
|
121
121
|
source: file.path,
|
|
122
122
|
target: imp.resolvedPath,
|
|
123
123
|
type: 'imports',
|
|
124
|
+
confidence: 1.0, // Import edges are always deterministic from AST
|
|
124
125
|
})
|
|
125
126
|
}
|
|
126
127
|
}
|
|
@@ -145,24 +146,27 @@ export class GraphBuilder {
|
|
|
145
146
|
// Try to resolve: first check imported names, then local functions
|
|
146
147
|
const simpleName = call.includes('.') ? call.split('.').pop()! : call
|
|
147
148
|
|
|
148
|
-
// Check if it's an imported function
|
|
149
|
+
// Check if it's an imported function (confidence 0.8 — resolved via import names)
|
|
149
150
|
const importedId = importedNames.get(simpleName) || importedNames.get(call)
|
|
150
151
|
if (importedId && graph.nodes.has(importedId)) {
|
|
151
152
|
graph.edges.push({
|
|
152
153
|
source: fn.id,
|
|
153
154
|
target: importedId,
|
|
154
155
|
type: 'calls',
|
|
156
|
+
confidence: 0.8, // Resolved through import names, not direct AST binding
|
|
155
157
|
})
|
|
156
158
|
continue
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
// Check if it's a local function in the same file
|
|
161
|
+
// Check if it's a local function in the same file (confidence 1.0 for exact, 0.5 for fuzzy)
|
|
160
162
|
const localId = `fn:${file.path}:${simpleName}`
|
|
161
163
|
if (graph.nodes.has(localId) && localId !== fn.id) {
|
|
164
|
+
// Direct local match — high confidence
|
|
162
165
|
graph.edges.push({
|
|
163
166
|
source: fn.id,
|
|
164
167
|
target: localId,
|
|
165
168
|
type: 'calls',
|
|
169
|
+
confidence: simpleName === call ? 1.0 : 0.5, // exact name = 1.0, dot-access strip = 0.5
|
|
166
170
|
})
|
|
167
171
|
}
|
|
168
172
|
}
|