@getmikk/core 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 +431 -0
- package/package.json +6 -2
- package/src/contract/contract-generator.ts +85 -85
- package/src/contract/contract-reader.ts +28 -28
- package/src/contract/contract-writer.ts +114 -114
- package/src/contract/index.ts +12 -12
- package/src/contract/lock-compiler.ts +221 -221
- package/src/contract/lock-reader.ts +34 -34
- package/src/contract/schema.ts +147 -147
- package/src/graph/cluster-detector.ts +312 -312
- package/src/graph/graph-builder.ts +211 -211
- package/src/graph/impact-analyzer.ts +55 -55
- package/src/graph/index.ts +4 -4
- package/src/graph/types.ts +59 -59
- package/src/hash/file-hasher.ts +30 -30
- package/src/hash/hash-store.ts +119 -119
- package/src/hash/index.ts +3 -3
- package/src/hash/tree-hasher.ts +20 -20
- package/src/index.ts +12 -12
- package/src/parser/base-parser.ts +16 -16
- package/src/parser/boundary-checker.ts +211 -211
- package/src/parser/index.ts +46 -46
- package/src/parser/types.ts +90 -90
- package/src/parser/typescript/ts-extractor.ts +543 -543
- package/src/parser/typescript/ts-parser.ts +41 -41
- package/src/parser/typescript/ts-resolver.ts +86 -86
- package/src/utils/errors.ts +42 -42
- package/src/utils/fs.ts +75 -75
- package/src/utils/fuzzy-match.ts +186 -186
- package/src/utils/logger.ts +36 -36
- package/src/utils/minimatch.ts +19 -19
- package/tests/contract.test.ts +134 -134
- package/tests/fixtures/simple-api/package.json +5 -5
- package/tests/fixtures/simple-api/src/auth/middleware.ts +9 -9
- package/tests/fixtures/simple-api/src/auth/verify.ts +6 -6
- package/tests/fixtures/simple-api/src/index.ts +9 -9
- package/tests/fixtures/simple-api/src/utils/jwt.ts +3 -3
- package/tests/fixtures/simple-api/tsconfig.json +8 -8
- package/tests/fuzzy-match.test.ts +142 -142
- package/tests/graph.test.ts +169 -169
- package/tests/hash.test.ts +49 -49
- package/tests/helpers.ts +83 -83
- package/tests/parser.test.ts +218 -218
- package/tsconfig.json +15 -15
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
import type { DependencyGraph, GraphNode, GraphEdge } from './types.js'
|
|
3
|
-
import type { ParsedFile, ParsedFunction, ParsedClass } from '../parser/types.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* GraphBuilder — takes parsed files and builds the dependency graph.
|
|
7
|
-
* Two-pass approach: first add all nodes, then add all edges.
|
|
8
|
-
*/
|
|
9
|
-
export class GraphBuilder {
|
|
10
|
-
/** Main entry point — takes all parsed files and returns the complete graph */
|
|
11
|
-
build(files: ParsedFile[]): DependencyGraph {
|
|
12
|
-
const graph: DependencyGraph = {
|
|
13
|
-
nodes: new Map(),
|
|
14
|
-
edges: [],
|
|
15
|
-
outEdges: new Map(),
|
|
16
|
-
inEdges: new Map(),
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// First pass: add all nodes
|
|
20
|
-
for (const file of files) {
|
|
21
|
-
this.addFileNode(graph, file)
|
|
22
|
-
for (const fn of file.functions) {
|
|
23
|
-
this.addFunctionNode(graph, fn)
|
|
24
|
-
}
|
|
25
|
-
for (const cls of file.classes || []) {
|
|
26
|
-
this.addClassNode(graph, cls, file.path)
|
|
27
|
-
}
|
|
28
|
-
for (const gen of file.generics || []) {
|
|
29
|
-
this.addGenericNode(graph, gen)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Second pass: add all edges
|
|
34
|
-
for (const file of files) {
|
|
35
|
-
this.addImportEdges(graph, file)
|
|
36
|
-
this.addCallEdges(graph, file)
|
|
37
|
-
this.addContainmentEdges(graph, file)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Third pass: build adjacency maps for fast lookup
|
|
41
|
-
this.buildAdjacencyMaps(graph)
|
|
42
|
-
|
|
43
|
-
return graph
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private addFileNode(graph: DependencyGraph, file: ParsedFile): void {
|
|
47
|
-
graph.nodes.set(file.path, {
|
|
48
|
-
id: file.path,
|
|
49
|
-
type: 'file',
|
|
50
|
-
label: path.basename(file.path),
|
|
51
|
-
file: file.path,
|
|
52
|
-
metadata: { hash: file.hash },
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private addFunctionNode(graph: DependencyGraph, fn: ParsedFunction): void {
|
|
57
|
-
graph.nodes.set(fn.id, {
|
|
58
|
-
id: fn.id,
|
|
59
|
-
type: 'function',
|
|
60
|
-
label: fn.name,
|
|
61
|
-
file: fn.file,
|
|
62
|
-
metadata: {
|
|
63
|
-
startLine: fn.startLine,
|
|
64
|
-
endLine: fn.endLine,
|
|
65
|
-
isExported: fn.isExported,
|
|
66
|
-
isAsync: fn.isAsync,
|
|
67
|
-
hash: fn.hash,
|
|
68
|
-
purpose: fn.purpose,
|
|
69
|
-
edgeCasesHandled: fn.edgeCasesHandled,
|
|
70
|
-
errorHandling: fn.errorHandling,
|
|
71
|
-
detailedLines: fn.detailedLines,
|
|
72
|
-
},
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private addClassNode(graph: DependencyGraph, cls: ParsedClass, filePath: string): void {
|
|
77
|
-
// Add a node for the class itself
|
|
78
|
-
graph.nodes.set(cls.id, {
|
|
79
|
-
id: cls.id,
|
|
80
|
-
type: 'class',
|
|
81
|
-
label: cls.name,
|
|
82
|
-
file: filePath,
|
|
83
|
-
metadata: {
|
|
84
|
-
startLine: cls.startLine,
|
|
85
|
-
endLine: cls.endLine,
|
|
86
|
-
isExported: cls.isExported,
|
|
87
|
-
purpose: cls.purpose,
|
|
88
|
-
edgeCasesHandled: cls.edgeCasesHandled,
|
|
89
|
-
errorHandling: cls.errorHandling,
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
// Add nodes for each method
|
|
93
|
-
for (const method of cls.methods) {
|
|
94
|
-
this.addFunctionNode(graph, method)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private addGenericNode(graph: DependencyGraph, gen: any): void {
|
|
99
|
-
graph.nodes.set(gen.id, {
|
|
100
|
-
id: gen.id,
|
|
101
|
-
type: 'generic',
|
|
102
|
-
label: gen.name,
|
|
103
|
-
file: gen.file,
|
|
104
|
-
metadata: {
|
|
105
|
-
startLine: gen.startLine,
|
|
106
|
-
endLine: gen.endLine,
|
|
107
|
-
isExported: gen.isExported,
|
|
108
|
-
purpose: gen.purpose,
|
|
109
|
-
hash: gen.type, // reusing hash or just storing the type string
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Creates edges for import statements: fileA imports fileB → edge(A, B, 'imports') */
|
|
115
|
-
private addImportEdges(graph: DependencyGraph, file: ParsedFile): void {
|
|
116
|
-
for (const imp of file.imports) {
|
|
117
|
-
if (imp.resolvedPath && graph.nodes.has(imp.resolvedPath)) {
|
|
118
|
-
graph.edges.push({
|
|
119
|
-
source: file.path,
|
|
120
|
-
target: imp.resolvedPath,
|
|
121
|
-
type: 'imports',
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Creates edges for function calls: fnA calls fnB → edge(A, B, 'calls') */
|
|
128
|
-
private addCallEdges(graph: DependencyGraph, file: ParsedFile): void {
|
|
129
|
-
// Build a map of import names to function IDs for resolving calls
|
|
130
|
-
const importedNames = new Map<string, string>()
|
|
131
|
-
for (const imp of file.imports) {
|
|
132
|
-
if (imp.resolvedPath) {
|
|
133
|
-
for (const name of imp.names) {
|
|
134
|
-
importedNames.set(name, `fn:${imp.resolvedPath}:${name}`)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const allFunctions = [...file.functions, ...file.classes.flatMap(c => c.methods)]
|
|
140
|
-
|
|
141
|
-
for (const fn of allFunctions) {
|
|
142
|
-
for (const call of fn.calls) {
|
|
143
|
-
// Try to resolve: first check imported names, then local functions
|
|
144
|
-
const simpleName = call.includes('.') ? call.split('.').pop()! : call
|
|
145
|
-
|
|
146
|
-
// Check if it's an imported function
|
|
147
|
-
const importedId = importedNames.get(simpleName) || importedNames.get(call)
|
|
148
|
-
if (importedId && graph.nodes.has(importedId)) {
|
|
149
|
-
graph.edges.push({
|
|
150
|
-
source: fn.id,
|
|
151
|
-
target: importedId,
|
|
152
|
-
type: 'calls',
|
|
153
|
-
})
|
|
154
|
-
continue
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Check if it's a local function in the same file
|
|
158
|
-
const localId = `fn:${file.path}:${simpleName}`
|
|
159
|
-
if (graph.nodes.has(localId) && localId !== fn.id) {
|
|
160
|
-
graph.edges.push({
|
|
161
|
-
source: fn.id,
|
|
162
|
-
target: localId,
|
|
163
|
-
type: 'calls',
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Creates containment edges: file contains function → edge(file, fn, 'contains') */
|
|
171
|
-
private addContainmentEdges(graph: DependencyGraph, file: ParsedFile): void {
|
|
172
|
-
for (const fn of file.functions) {
|
|
173
|
-
graph.edges.push({
|
|
174
|
-
source: file.path,
|
|
175
|
-
target: fn.id,
|
|
176
|
-
type: 'contains',
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
for (const cls of file.classes) {
|
|
180
|
-
graph.edges.push({
|
|
181
|
-
source: file.path,
|
|
182
|
-
target: cls.id,
|
|
183
|
-
type: 'contains',
|
|
184
|
-
})
|
|
185
|
-
for (const method of cls.methods) {
|
|
186
|
-
graph.edges.push({
|
|
187
|
-
source: cls.id,
|
|
188
|
-
target: method.id,
|
|
189
|
-
type: 'contains',
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Build adjacency maps from edge list for O(1) lookups */
|
|
196
|
-
private buildAdjacencyMaps(graph: DependencyGraph): void {
|
|
197
|
-
for (const edge of graph.edges) {
|
|
198
|
-
// outEdges
|
|
199
|
-
if (!graph.outEdges.has(edge.source)) {
|
|
200
|
-
graph.outEdges.set(edge.source, [])
|
|
201
|
-
}
|
|
202
|
-
graph.outEdges.get(edge.source)!.push(edge)
|
|
203
|
-
|
|
204
|
-
// inEdges
|
|
205
|
-
if (!graph.inEdges.has(edge.target)) {
|
|
206
|
-
graph.inEdges.set(edge.target, [])
|
|
207
|
-
}
|
|
208
|
-
graph.inEdges.get(edge.target)!.push(edge)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import type { DependencyGraph, GraphNode, GraphEdge } from './types.js'
|
|
3
|
+
import type { ParsedFile, ParsedFunction, ParsedClass } from '../parser/types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GraphBuilder — takes parsed files and builds the dependency graph.
|
|
7
|
+
* Two-pass approach: first add all nodes, then add all edges.
|
|
8
|
+
*/
|
|
9
|
+
export class GraphBuilder {
|
|
10
|
+
/** Main entry point — takes all parsed files and returns the complete graph */
|
|
11
|
+
build(files: ParsedFile[]): DependencyGraph {
|
|
12
|
+
const graph: DependencyGraph = {
|
|
13
|
+
nodes: new Map(),
|
|
14
|
+
edges: [],
|
|
15
|
+
outEdges: new Map(),
|
|
16
|
+
inEdges: new Map(),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// First pass: add all nodes
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
this.addFileNode(graph, file)
|
|
22
|
+
for (const fn of file.functions) {
|
|
23
|
+
this.addFunctionNode(graph, fn)
|
|
24
|
+
}
|
|
25
|
+
for (const cls of file.classes || []) {
|
|
26
|
+
this.addClassNode(graph, cls, file.path)
|
|
27
|
+
}
|
|
28
|
+
for (const gen of file.generics || []) {
|
|
29
|
+
this.addGenericNode(graph, gen)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Second pass: add all edges
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
this.addImportEdges(graph, file)
|
|
36
|
+
this.addCallEdges(graph, file)
|
|
37
|
+
this.addContainmentEdges(graph, file)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Third pass: build adjacency maps for fast lookup
|
|
41
|
+
this.buildAdjacencyMaps(graph)
|
|
42
|
+
|
|
43
|
+
return graph
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private addFileNode(graph: DependencyGraph, file: ParsedFile): void {
|
|
47
|
+
graph.nodes.set(file.path, {
|
|
48
|
+
id: file.path,
|
|
49
|
+
type: 'file',
|
|
50
|
+
label: path.basename(file.path),
|
|
51
|
+
file: file.path,
|
|
52
|
+
metadata: { hash: file.hash },
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private addFunctionNode(graph: DependencyGraph, fn: ParsedFunction): void {
|
|
57
|
+
graph.nodes.set(fn.id, {
|
|
58
|
+
id: fn.id,
|
|
59
|
+
type: 'function',
|
|
60
|
+
label: fn.name,
|
|
61
|
+
file: fn.file,
|
|
62
|
+
metadata: {
|
|
63
|
+
startLine: fn.startLine,
|
|
64
|
+
endLine: fn.endLine,
|
|
65
|
+
isExported: fn.isExported,
|
|
66
|
+
isAsync: fn.isAsync,
|
|
67
|
+
hash: fn.hash,
|
|
68
|
+
purpose: fn.purpose,
|
|
69
|
+
edgeCasesHandled: fn.edgeCasesHandled,
|
|
70
|
+
errorHandling: fn.errorHandling,
|
|
71
|
+
detailedLines: fn.detailedLines,
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private addClassNode(graph: DependencyGraph, cls: ParsedClass, filePath: string): void {
|
|
77
|
+
// Add a node for the class itself
|
|
78
|
+
graph.nodes.set(cls.id, {
|
|
79
|
+
id: cls.id,
|
|
80
|
+
type: 'class',
|
|
81
|
+
label: cls.name,
|
|
82
|
+
file: filePath,
|
|
83
|
+
metadata: {
|
|
84
|
+
startLine: cls.startLine,
|
|
85
|
+
endLine: cls.endLine,
|
|
86
|
+
isExported: cls.isExported,
|
|
87
|
+
purpose: cls.purpose,
|
|
88
|
+
edgeCasesHandled: cls.edgeCasesHandled,
|
|
89
|
+
errorHandling: cls.errorHandling,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
// Add nodes for each method
|
|
93
|
+
for (const method of cls.methods) {
|
|
94
|
+
this.addFunctionNode(graph, method)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private addGenericNode(graph: DependencyGraph, gen: any): void {
|
|
99
|
+
graph.nodes.set(gen.id, {
|
|
100
|
+
id: gen.id,
|
|
101
|
+
type: 'generic',
|
|
102
|
+
label: gen.name,
|
|
103
|
+
file: gen.file,
|
|
104
|
+
metadata: {
|
|
105
|
+
startLine: gen.startLine,
|
|
106
|
+
endLine: gen.endLine,
|
|
107
|
+
isExported: gen.isExported,
|
|
108
|
+
purpose: gen.purpose,
|
|
109
|
+
hash: gen.type, // reusing hash or just storing the type string
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Creates edges for import statements: fileA imports fileB → edge(A, B, 'imports') */
|
|
115
|
+
private addImportEdges(graph: DependencyGraph, file: ParsedFile): void {
|
|
116
|
+
for (const imp of file.imports) {
|
|
117
|
+
if (imp.resolvedPath && graph.nodes.has(imp.resolvedPath)) {
|
|
118
|
+
graph.edges.push({
|
|
119
|
+
source: file.path,
|
|
120
|
+
target: imp.resolvedPath,
|
|
121
|
+
type: 'imports',
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Creates edges for function calls: fnA calls fnB → edge(A, B, 'calls') */
|
|
128
|
+
private addCallEdges(graph: DependencyGraph, file: ParsedFile): void {
|
|
129
|
+
// Build a map of import names to function IDs for resolving calls
|
|
130
|
+
const importedNames = new Map<string, string>()
|
|
131
|
+
for (const imp of file.imports) {
|
|
132
|
+
if (imp.resolvedPath) {
|
|
133
|
+
for (const name of imp.names) {
|
|
134
|
+
importedNames.set(name, `fn:${imp.resolvedPath}:${name}`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const allFunctions = [...file.functions, ...file.classes.flatMap(c => c.methods)]
|
|
140
|
+
|
|
141
|
+
for (const fn of allFunctions) {
|
|
142
|
+
for (const call of fn.calls) {
|
|
143
|
+
// Try to resolve: first check imported names, then local functions
|
|
144
|
+
const simpleName = call.includes('.') ? call.split('.').pop()! : call
|
|
145
|
+
|
|
146
|
+
// Check if it's an imported function
|
|
147
|
+
const importedId = importedNames.get(simpleName) || importedNames.get(call)
|
|
148
|
+
if (importedId && graph.nodes.has(importedId)) {
|
|
149
|
+
graph.edges.push({
|
|
150
|
+
source: fn.id,
|
|
151
|
+
target: importedId,
|
|
152
|
+
type: 'calls',
|
|
153
|
+
})
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if it's a local function in the same file
|
|
158
|
+
const localId = `fn:${file.path}:${simpleName}`
|
|
159
|
+
if (graph.nodes.has(localId) && localId !== fn.id) {
|
|
160
|
+
graph.edges.push({
|
|
161
|
+
source: fn.id,
|
|
162
|
+
target: localId,
|
|
163
|
+
type: 'calls',
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Creates containment edges: file contains function → edge(file, fn, 'contains') */
|
|
171
|
+
private addContainmentEdges(graph: DependencyGraph, file: ParsedFile): void {
|
|
172
|
+
for (const fn of file.functions) {
|
|
173
|
+
graph.edges.push({
|
|
174
|
+
source: file.path,
|
|
175
|
+
target: fn.id,
|
|
176
|
+
type: 'contains',
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
for (const cls of file.classes) {
|
|
180
|
+
graph.edges.push({
|
|
181
|
+
source: file.path,
|
|
182
|
+
target: cls.id,
|
|
183
|
+
type: 'contains',
|
|
184
|
+
})
|
|
185
|
+
for (const method of cls.methods) {
|
|
186
|
+
graph.edges.push({
|
|
187
|
+
source: cls.id,
|
|
188
|
+
target: method.id,
|
|
189
|
+
type: 'contains',
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Build adjacency maps from edge list for O(1) lookups */
|
|
196
|
+
private buildAdjacencyMaps(graph: DependencyGraph): void {
|
|
197
|
+
for (const edge of graph.edges) {
|
|
198
|
+
// outEdges
|
|
199
|
+
if (!graph.outEdges.has(edge.source)) {
|
|
200
|
+
graph.outEdges.set(edge.source, [])
|
|
201
|
+
}
|
|
202
|
+
graph.outEdges.get(edge.source)!.push(edge)
|
|
203
|
+
|
|
204
|
+
// inEdges
|
|
205
|
+
if (!graph.inEdges.has(edge.target)) {
|
|
206
|
+
graph.inEdges.set(edge.target, [])
|
|
207
|
+
}
|
|
208
|
+
graph.inEdges.get(edge.target)!.push(edge)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
import type { DependencyGraph, ImpactResult } from './types.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ImpactAnalyzer — Given changed nodes, walks the graph backwards (BFS)
|
|
5
|
-
* to find everything that depends on them.
|
|
6
|
-
* Powers "what breaks if I change X?"
|
|
7
|
-
*/
|
|
8
|
-
export class ImpactAnalyzer {
|
|
9
|
-
constructor(private graph: DependencyGraph) { }
|
|
10
|
-
|
|
11
|
-
/** Given a list of changed node IDs, find everything impacted */
|
|
12
|
-
analyze(changedNodeIds: string[]): ImpactResult {
|
|
13
|
-
const visited = new Set<string>()
|
|
14
|
-
const queue: { id: string; depth: number }[] = changedNodeIds.map(id => ({ id, depth: 0 }))
|
|
15
|
-
let maxDepth = 0
|
|
16
|
-
|
|
17
|
-
while (queue.length > 0) {
|
|
18
|
-
const { id: current, depth } = queue.shift()!
|
|
19
|
-
if (visited.has(current)) continue
|
|
20
|
-
visited.add(current)
|
|
21
|
-
maxDepth = Math.max(maxDepth, depth)
|
|
22
|
-
|
|
23
|
-
// Find everything that depends on current (incoming edges)
|
|
24
|
-
const dependents = this.graph.inEdges.get(current) || []
|
|
25
|
-
for (const edge of dependents) {
|
|
26
|
-
if (!visited.has(edge.source) && edge.type !== 'contains') {
|
|
27
|
-
queue.push({ id: edge.source, depth: depth + 1 })
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const impacted = [...visited].filter(id => !changedNodeIds.includes(id))
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
changed: changedNodeIds,
|
|
36
|
-
impacted,
|
|
37
|
-
depth: maxDepth,
|
|
38
|
-
confidence: this.computeConfidence(impacted.length, maxDepth),
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* How confident are we in this impact analysis?
|
|
44
|
-
* High = few nodes affected, shallow depth
|
|
45
|
-
* Low = many nodes affected, deep chains
|
|
46
|
-
*/
|
|
47
|
-
private computeConfidence(
|
|
48
|
-
impactedCount: number,
|
|
49
|
-
depth: number
|
|
50
|
-
): 'high' | 'medium' | 'low' {
|
|
51
|
-
if (impactedCount < 5 && depth < 3) return 'high'
|
|
52
|
-
if (impactedCount < 20 && depth < 6) return 'medium'
|
|
53
|
-
return 'low'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
1
|
+
import type { DependencyGraph, ImpactResult } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ImpactAnalyzer — Given changed nodes, walks the graph backwards (BFS)
|
|
5
|
+
* to find everything that depends on them.
|
|
6
|
+
* Powers "what breaks if I change X?"
|
|
7
|
+
*/
|
|
8
|
+
export class ImpactAnalyzer {
|
|
9
|
+
constructor(private graph: DependencyGraph) { }
|
|
10
|
+
|
|
11
|
+
/** Given a list of changed node IDs, find everything impacted */
|
|
12
|
+
analyze(changedNodeIds: string[]): ImpactResult {
|
|
13
|
+
const visited = new Set<string>()
|
|
14
|
+
const queue: { id: string; depth: number }[] = changedNodeIds.map(id => ({ id, depth: 0 }))
|
|
15
|
+
let maxDepth = 0
|
|
16
|
+
|
|
17
|
+
while (queue.length > 0) {
|
|
18
|
+
const { id: current, depth } = queue.shift()!
|
|
19
|
+
if (visited.has(current)) continue
|
|
20
|
+
visited.add(current)
|
|
21
|
+
maxDepth = Math.max(maxDepth, depth)
|
|
22
|
+
|
|
23
|
+
// Find everything that depends on current (incoming edges)
|
|
24
|
+
const dependents = this.graph.inEdges.get(current) || []
|
|
25
|
+
for (const edge of dependents) {
|
|
26
|
+
if (!visited.has(edge.source) && edge.type !== 'contains') {
|
|
27
|
+
queue.push({ id: edge.source, depth: depth + 1 })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const impacted = [...visited].filter(id => !changedNodeIds.includes(id))
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
changed: changedNodeIds,
|
|
36
|
+
impacted,
|
|
37
|
+
depth: maxDepth,
|
|
38
|
+
confidence: this.computeConfidence(impacted.length, maxDepth),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How confident are we in this impact analysis?
|
|
44
|
+
* High = few nodes affected, shallow depth
|
|
45
|
+
* Low = many nodes affected, deep chains
|
|
46
|
+
*/
|
|
47
|
+
private computeConfidence(
|
|
48
|
+
impactedCount: number,
|
|
49
|
+
depth: number
|
|
50
|
+
): 'high' | 'medium' | 'low' {
|
|
51
|
+
if (impactedCount < 5 && depth < 3) return 'high'
|
|
52
|
+
if (impactedCount < 20 && depth < 6) return 'medium'
|
|
53
|
+
return 'low'
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/graph/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { DependencyGraph, GraphNode, GraphEdge, ImpactResult, NodeType, EdgeType, ModuleCluster } from './types.js'
|
|
2
|
-
export { GraphBuilder } from './graph-builder.js'
|
|
3
|
-
export { ImpactAnalyzer } from './impact-analyzer.js'
|
|
4
|
-
export { ClusterDetector } from './cluster-detector.js'
|
|
1
|
+
export type { DependencyGraph, GraphNode, GraphEdge, ImpactResult, NodeType, EdgeType, ModuleCluster } from './types.js'
|
|
2
|
+
export { GraphBuilder } from './graph-builder.js'
|
|
3
|
+
export { ImpactAnalyzer } from './impact-analyzer.js'
|
|
4
|
+
export { ClusterDetector } from './cluster-detector.js'
|
package/src/graph/types.ts
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Graph types — nodes, edges, and the dependency graph itself.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export type NodeType = 'function' | 'file' | 'module' | 'class' | 'generic'
|
|
6
|
-
export type EdgeType = 'calls' | 'imports' | 'exports' | 'contains'
|
|
7
|
-
|
|
8
|
-
/** A single node in the dependency graph */
|
|
9
|
-
export interface GraphNode {
|
|
10
|
-
id: string // "fn:src/auth/verify.ts:verifyToken"
|
|
11
|
-
type: NodeType
|
|
12
|
-
label: string // "verifyToken"
|
|
13
|
-
file: string // "src/auth/verify.ts"
|
|
14
|
-
moduleId?: string // "auth" — which declared module this belongs to
|
|
15
|
-
metadata: {
|
|
16
|
-
startLine?: number
|
|
17
|
-
endLine?: number
|
|
18
|
-
isExported?: boolean
|
|
19
|
-
isAsync?: boolean
|
|
20
|
-
hash?: string
|
|
21
|
-
purpose?: string
|
|
22
|
-
edgeCasesHandled?: string[]
|
|
23
|
-
errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[]
|
|
24
|
-
detailedLines?: { startLine: number; endLine: number; blockType: string }[]
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** A single edge in the dependency graph */
|
|
29
|
-
export interface GraphEdge {
|
|
30
|
-
source: string // "fn:src/auth/verify.ts:verifyToken"
|
|
31
|
-
target: string // "fn:src/utils/jwt.ts:jwtDecode"
|
|
32
|
-
type: EdgeType
|
|
33
|
-
weight?: number // How often this call happens (for coupling metrics)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The full dependency graph */
|
|
37
|
-
export interface DependencyGraph {
|
|
38
|
-
nodes: Map<string, GraphNode>
|
|
39
|
-
edges: GraphEdge[]
|
|
40
|
-
outEdges: Map<string, GraphEdge[]> // node → [edges going out]
|
|
41
|
-
inEdges: Map<string, GraphEdge[]> // node → [edges coming in]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Result of impact analysis */
|
|
45
|
-
export interface ImpactResult {
|
|
46
|
-
changed: string[] // The directly changed nodes
|
|
47
|
-
impacted: string[] // Everything that depends on changed nodes
|
|
48
|
-
depth: number // How many hops from change to furthest impact
|
|
49
|
-
confidence: 'high' | 'medium' | 'low'
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** A cluster of files that naturally belong together */
|
|
53
|
-
export interface ModuleCluster {
|
|
54
|
-
id: string
|
|
55
|
-
files: string[]
|
|
56
|
-
confidence: number // 0.0 to 1.0
|
|
57
|
-
suggestedName: string // inferred from folder names
|
|
58
|
-
functions: string[] // function IDs in this cluster
|
|
59
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Graph types — nodes, edges, and the dependency graph itself.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type NodeType = 'function' | 'file' | 'module' | 'class' | 'generic'
|
|
6
|
+
export type EdgeType = 'calls' | 'imports' | 'exports' | 'contains'
|
|
7
|
+
|
|
8
|
+
/** A single node in the dependency graph */
|
|
9
|
+
export interface GraphNode {
|
|
10
|
+
id: string // "fn:src/auth/verify.ts:verifyToken"
|
|
11
|
+
type: NodeType
|
|
12
|
+
label: string // "verifyToken"
|
|
13
|
+
file: string // "src/auth/verify.ts"
|
|
14
|
+
moduleId?: string // "auth" — which declared module this belongs to
|
|
15
|
+
metadata: {
|
|
16
|
+
startLine?: number
|
|
17
|
+
endLine?: number
|
|
18
|
+
isExported?: boolean
|
|
19
|
+
isAsync?: boolean
|
|
20
|
+
hash?: string
|
|
21
|
+
purpose?: string
|
|
22
|
+
edgeCasesHandled?: string[]
|
|
23
|
+
errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[]
|
|
24
|
+
detailedLines?: { startLine: number; endLine: number; blockType: string }[]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A single edge in the dependency graph */
|
|
29
|
+
export interface GraphEdge {
|
|
30
|
+
source: string // "fn:src/auth/verify.ts:verifyToken"
|
|
31
|
+
target: string // "fn:src/utils/jwt.ts:jwtDecode"
|
|
32
|
+
type: EdgeType
|
|
33
|
+
weight?: number // How often this call happens (for coupling metrics)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The full dependency graph */
|
|
37
|
+
export interface DependencyGraph {
|
|
38
|
+
nodes: Map<string, GraphNode>
|
|
39
|
+
edges: GraphEdge[]
|
|
40
|
+
outEdges: Map<string, GraphEdge[]> // node → [edges going out]
|
|
41
|
+
inEdges: Map<string, GraphEdge[]> // node → [edges coming in]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Result of impact analysis */
|
|
45
|
+
export interface ImpactResult {
|
|
46
|
+
changed: string[] // The directly changed nodes
|
|
47
|
+
impacted: string[] // Everything that depends on changed nodes
|
|
48
|
+
depth: number // How many hops from change to furthest impact
|
|
49
|
+
confidence: 'high' | 'medium' | 'low'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A cluster of files that naturally belong together */
|
|
53
|
+
export interface ModuleCluster {
|
|
54
|
+
id: string
|
|
55
|
+
files: string[]
|
|
56
|
+
confidence: number // 0.0 to 1.0
|
|
57
|
+
suggestedName: string // inferred from folder names
|
|
58
|
+
functions: string[] // function IDs in this cluster
|
|
59
|
+
}
|