@getmikk/core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +431 -0
  2. package/package.json +6 -2
  3. package/src/contract/contract-generator.ts +85 -85
  4. package/src/contract/contract-reader.ts +28 -28
  5. package/src/contract/contract-writer.ts +114 -114
  6. package/src/contract/index.ts +12 -12
  7. package/src/contract/lock-compiler.ts +221 -221
  8. package/src/contract/lock-reader.ts +34 -34
  9. package/src/contract/schema.ts +147 -147
  10. package/src/graph/cluster-detector.ts +312 -312
  11. package/src/graph/graph-builder.ts +211 -211
  12. package/src/graph/impact-analyzer.ts +55 -55
  13. package/src/graph/index.ts +4 -4
  14. package/src/graph/types.ts +59 -59
  15. package/src/hash/file-hasher.ts +30 -30
  16. package/src/hash/hash-store.ts +119 -119
  17. package/src/hash/index.ts +3 -3
  18. package/src/hash/tree-hasher.ts +20 -20
  19. package/src/index.ts +12 -12
  20. package/src/parser/base-parser.ts +16 -16
  21. package/src/parser/boundary-checker.ts +211 -211
  22. package/src/parser/index.ts +46 -46
  23. package/src/parser/types.ts +90 -90
  24. package/src/parser/typescript/ts-extractor.ts +543 -543
  25. package/src/parser/typescript/ts-parser.ts +41 -41
  26. package/src/parser/typescript/ts-resolver.ts +86 -86
  27. package/src/utils/errors.ts +42 -42
  28. package/src/utils/fs.ts +75 -75
  29. package/src/utils/fuzzy-match.ts +186 -186
  30. package/src/utils/logger.ts +36 -36
  31. package/src/utils/minimatch.ts +19 -19
  32. package/tests/contract.test.ts +134 -134
  33. package/tests/fixtures/simple-api/package.json +5 -5
  34. package/tests/fixtures/simple-api/src/auth/middleware.ts +9 -9
  35. package/tests/fixtures/simple-api/src/auth/verify.ts +6 -6
  36. package/tests/fixtures/simple-api/src/index.ts +9 -9
  37. package/tests/fixtures/simple-api/src/utils/jwt.ts +3 -3
  38. package/tests/fixtures/simple-api/tsconfig.json +8 -8
  39. package/tests/fuzzy-match.test.ts +142 -142
  40. package/tests/graph.test.ts +169 -169
  41. package/tests/hash.test.ts +49 -49
  42. package/tests/helpers.ts +83 -83
  43. package/tests/parser.test.ts +218 -218
  44. package/tsconfig.json +15 -15
@@ -1,169 +1,169 @@
1
- import { describe, it, expect } from 'bun:test'
2
- import { GraphBuilder } from '../src/graph/graph-builder'
3
- import { ImpactAnalyzer } from '../src/graph/impact-analyzer'
4
- import { ClusterDetector } from '../src/graph/cluster-detector'
5
- import { mockParsedFile, mockFunction, mockImport, buildTestGraph } from './helpers'
6
-
7
- describe('GraphBuilder', () => {
8
- const builder = new GraphBuilder()
9
-
10
- it('creates nodes for files', () => {
11
- const files = [mockParsedFile('src/auth.ts')]
12
- const graph = builder.build(files)
13
- expect(graph.nodes.has('src/auth.ts')).toBe(true)
14
- expect(graph.nodes.get('src/auth.ts')!.type).toBe('file')
15
- })
16
-
17
- it('creates nodes for functions', () => {
18
- const files = [
19
- mockParsedFile('src/auth.ts', [mockFunction('verifyToken', [], 'src/auth.ts')]),
20
- ]
21
- const graph = builder.build(files)
22
- expect(graph.nodes.has('fn:src/auth.ts:verifyToken')).toBe(true)
23
- expect(graph.nodes.get('fn:src/auth.ts:verifyToken')!.type).toBe('function')
24
- })
25
-
26
- it('creates edges for imports', () => {
27
- const files = [
28
- mockParsedFile(
29
- 'src/auth.ts',
30
- [mockFunction('verifyToken', [], 'src/auth.ts')],
31
- [mockImport('../utils/jwt', ['jwtDecode'], 'src/utils/jwt.ts')]
32
- ),
33
- mockParsedFile('src/utils/jwt.ts', [mockFunction('jwtDecode', [], 'src/utils/jwt.ts')]),
34
- ]
35
- const graph = builder.build(files)
36
- const importEdges = graph.edges.filter(e => e.type === 'imports')
37
- expect(importEdges.length).toBeGreaterThanOrEqual(1)
38
- expect(importEdges[0].source).toBe('src/auth.ts')
39
- expect(importEdges[0].target).toBe('src/utils/jwt.ts')
40
- })
41
-
42
- it('creates edges for function calls via imports', () => {
43
- const files = [
44
- mockParsedFile(
45
- 'src/auth.ts',
46
- [mockFunction('verifyToken', ['jwtDecode'], 'src/auth.ts')],
47
- [mockImport('../utils/jwt', ['jwtDecode'], 'src/utils/jwt.ts')]
48
- ),
49
- mockParsedFile('src/utils/jwt.ts', [mockFunction('jwtDecode', [], 'src/utils/jwt.ts')]),
50
- ]
51
- const graph = builder.build(files)
52
- const callEdges = graph.edges.filter(e => e.type === 'calls')
53
- expect(callEdges.length).toBeGreaterThanOrEqual(1)
54
- expect(callEdges[0].source).toBe('fn:src/auth.ts:verifyToken')
55
- expect(callEdges[0].target).toBe('fn:src/utils/jwt.ts:jwtDecode')
56
- })
57
-
58
- it('creates containment edges', () => {
59
- const files = [
60
- mockParsedFile('src/auth.ts', [mockFunction('verifyToken', [], 'src/auth.ts')]),
61
- ]
62
- const graph = builder.build(files)
63
- const containEdges = graph.edges.filter(e => e.type === 'contains')
64
- expect(containEdges.length).toBeGreaterThanOrEqual(1)
65
- expect(containEdges[0].source).toBe('src/auth.ts')
66
- expect(containEdges[0].target).toBe('fn:src/auth.ts:verifyToken')
67
- })
68
-
69
- it('builds adjacency maps', () => {
70
- const files = [
71
- mockParsedFile('src/auth.ts', [mockFunction('verifyToken', [], 'src/auth.ts')]),
72
- ]
73
- const graph = builder.build(files)
74
- expect(graph.outEdges.has('src/auth.ts')).toBe(true)
75
- expect(graph.inEdges.has('fn:src/auth.ts:verifyToken')).toBe(true)
76
- })
77
- })
78
-
79
- describe('ImpactAnalyzer', () => {
80
- it('finds direct dependents', () => {
81
- const graph = buildTestGraph([
82
- ['A', 'B'],
83
- ['B', 'nothing'],
84
- ])
85
- const analyzer = new ImpactAnalyzer(graph)
86
- const result = analyzer.analyze(['fn:src/B.ts:B'])
87
- expect(result.impacted).toContain('fn:src/A.ts:A')
88
- })
89
-
90
- it('finds transitive dependents', () => {
91
- const graph = buildTestGraph([
92
- ['A', 'B'],
93
- ['B', 'C'],
94
- ['C', 'nothing'],
95
- ])
96
- const analyzer = new ImpactAnalyzer(graph)
97
- const result = analyzer.analyze(['fn:src/C.ts:C'])
98
- expect(result.impacted).toContain('fn:src/B.ts:B')
99
- expect(result.impacted).toContain('fn:src/A.ts:A')
100
- })
101
-
102
- it('reports correct depth', () => {
103
- const graph = buildTestGraph([
104
- ['A', 'B'],
105
- ['B', 'C'],
106
- ['C', 'D'],
107
- ['D', 'nothing'],
108
- ])
109
- const analyzer = new ImpactAnalyzer(graph)
110
- const result = analyzer.analyze(['fn:src/D.ts:D'])
111
- expect(result.depth).toBeGreaterThanOrEqual(3)
112
- })
113
-
114
- it('assigns high confidence for small impacts', () => {
115
- const graph = buildTestGraph([
116
- ['A', 'B'],
117
- ['B', 'nothing'],
118
- ])
119
- const analyzer = new ImpactAnalyzer(graph)
120
- const result = analyzer.analyze(['fn:src/B.ts:B'])
121
- expect(result.confidence).toBe('high')
122
- })
123
-
124
- it('does not include changed nodes in impacted', () => {
125
- const graph = buildTestGraph([
126
- ['A', 'B'],
127
- ['B', 'nothing'],
128
- ])
129
- const analyzer = new ImpactAnalyzer(graph)
130
- const result = analyzer.analyze(['fn:src/B.ts:B'])
131
- expect(result.impacted).not.toContain('fn:src/B.ts:B')
132
- expect(result.changed).toContain('fn:src/B.ts:B')
133
- })
134
- })
135
-
136
- describe('ClusterDetector', () => {
137
- it('groups files by directory', () => {
138
- const files = [
139
- mockParsedFile('src/auth/verify.ts',
140
- [mockFunction('verifyToken', ['authMiddleware'], 'src/auth/verify.ts')],
141
- [mockImport('./middleware', ['authMiddleware'], 'src/auth/middleware.ts')]
142
- ),
143
- mockParsedFile('src/auth/middleware.ts',
144
- [mockFunction('authMiddleware', ['verifyToken'], 'src/auth/middleware.ts')],
145
- [mockImport('./verify', ['verifyToken'], 'src/auth/verify.ts')]
146
- ),
147
- mockParsedFile('src/payments/charge.ts', [mockFunction('charge', [], 'src/payments/charge.ts')]),
148
- ]
149
- const graph = new GraphBuilder().build(files)
150
- // Use minClusterSize=1 so even single-file groups appear
151
- const detector = new ClusterDetector(graph, 1)
152
- const clusters = detector.detect()
153
- expect(clusters.length).toBeGreaterThanOrEqual(2)
154
- const authCluster = clusters.find(c => c.id === 'auth')
155
- expect(authCluster).toBeDefined()
156
- expect(authCluster!.files).toHaveLength(2)
157
- })
158
-
159
- it('computes confidence scores', () => {
160
- const files = [
161
- mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
162
- ]
163
- const graph = new GraphBuilder().build(files)
164
- const detector = new ClusterDetector(graph, 1)
165
- const score = detector.computeClusterConfidence(['src/auth/verify.ts'])
166
- expect(score).toBeGreaterThanOrEqual(0)
167
- expect(score).toBeLessThanOrEqual(1)
168
- })
169
- })
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { GraphBuilder } from '../src/graph/graph-builder'
3
+ import { ImpactAnalyzer } from '../src/graph/impact-analyzer'
4
+ import { ClusterDetector } from '../src/graph/cluster-detector'
5
+ import { mockParsedFile, mockFunction, mockImport, buildTestGraph } from './helpers'
6
+
7
+ describe('GraphBuilder', () => {
8
+ const builder = new GraphBuilder()
9
+
10
+ it('creates nodes for files', () => {
11
+ const files = [mockParsedFile('src/auth.ts')]
12
+ const graph = builder.build(files)
13
+ expect(graph.nodes.has('src/auth.ts')).toBe(true)
14
+ expect(graph.nodes.get('src/auth.ts')!.type).toBe('file')
15
+ })
16
+
17
+ it('creates nodes for functions', () => {
18
+ const files = [
19
+ mockParsedFile('src/auth.ts', [mockFunction('verifyToken', [], 'src/auth.ts')]),
20
+ ]
21
+ const graph = builder.build(files)
22
+ expect(graph.nodes.has('fn:src/auth.ts:verifyToken')).toBe(true)
23
+ expect(graph.nodes.get('fn:src/auth.ts:verifyToken')!.type).toBe('function')
24
+ })
25
+
26
+ it('creates edges for imports', () => {
27
+ const files = [
28
+ mockParsedFile(
29
+ 'src/auth.ts',
30
+ [mockFunction('verifyToken', [], 'src/auth.ts')],
31
+ [mockImport('../utils/jwt', ['jwtDecode'], 'src/utils/jwt.ts')]
32
+ ),
33
+ mockParsedFile('src/utils/jwt.ts', [mockFunction('jwtDecode', [], 'src/utils/jwt.ts')]),
34
+ ]
35
+ const graph = builder.build(files)
36
+ const importEdges = graph.edges.filter(e => e.type === 'imports')
37
+ expect(importEdges.length).toBeGreaterThanOrEqual(1)
38
+ expect(importEdges[0].source).toBe('src/auth.ts')
39
+ expect(importEdges[0].target).toBe('src/utils/jwt.ts')
40
+ })
41
+
42
+ it('creates edges for function calls via imports', () => {
43
+ const files = [
44
+ mockParsedFile(
45
+ 'src/auth.ts',
46
+ [mockFunction('verifyToken', ['jwtDecode'], 'src/auth.ts')],
47
+ [mockImport('../utils/jwt', ['jwtDecode'], 'src/utils/jwt.ts')]
48
+ ),
49
+ mockParsedFile('src/utils/jwt.ts', [mockFunction('jwtDecode', [], 'src/utils/jwt.ts')]),
50
+ ]
51
+ const graph = builder.build(files)
52
+ const callEdges = graph.edges.filter(e => e.type === 'calls')
53
+ expect(callEdges.length).toBeGreaterThanOrEqual(1)
54
+ expect(callEdges[0].source).toBe('fn:src/auth.ts:verifyToken')
55
+ expect(callEdges[0].target).toBe('fn:src/utils/jwt.ts:jwtDecode')
56
+ })
57
+
58
+ it('creates containment edges', () => {
59
+ const files = [
60
+ mockParsedFile('src/auth.ts', [mockFunction('verifyToken', [], 'src/auth.ts')]),
61
+ ]
62
+ const graph = builder.build(files)
63
+ const containEdges = graph.edges.filter(e => e.type === 'contains')
64
+ expect(containEdges.length).toBeGreaterThanOrEqual(1)
65
+ expect(containEdges[0].source).toBe('src/auth.ts')
66
+ expect(containEdges[0].target).toBe('fn:src/auth.ts:verifyToken')
67
+ })
68
+
69
+ it('builds adjacency maps', () => {
70
+ const files = [
71
+ mockParsedFile('src/auth.ts', [mockFunction('verifyToken', [], 'src/auth.ts')]),
72
+ ]
73
+ const graph = builder.build(files)
74
+ expect(graph.outEdges.has('src/auth.ts')).toBe(true)
75
+ expect(graph.inEdges.has('fn:src/auth.ts:verifyToken')).toBe(true)
76
+ })
77
+ })
78
+
79
+ describe('ImpactAnalyzer', () => {
80
+ it('finds direct dependents', () => {
81
+ const graph = buildTestGraph([
82
+ ['A', 'B'],
83
+ ['B', 'nothing'],
84
+ ])
85
+ const analyzer = new ImpactAnalyzer(graph)
86
+ const result = analyzer.analyze(['fn:src/B.ts:B'])
87
+ expect(result.impacted).toContain('fn:src/A.ts:A')
88
+ })
89
+
90
+ it('finds transitive dependents', () => {
91
+ const graph = buildTestGraph([
92
+ ['A', 'B'],
93
+ ['B', 'C'],
94
+ ['C', 'nothing'],
95
+ ])
96
+ const analyzer = new ImpactAnalyzer(graph)
97
+ const result = analyzer.analyze(['fn:src/C.ts:C'])
98
+ expect(result.impacted).toContain('fn:src/B.ts:B')
99
+ expect(result.impacted).toContain('fn:src/A.ts:A')
100
+ })
101
+
102
+ it('reports correct depth', () => {
103
+ const graph = buildTestGraph([
104
+ ['A', 'B'],
105
+ ['B', 'C'],
106
+ ['C', 'D'],
107
+ ['D', 'nothing'],
108
+ ])
109
+ const analyzer = new ImpactAnalyzer(graph)
110
+ const result = analyzer.analyze(['fn:src/D.ts:D'])
111
+ expect(result.depth).toBeGreaterThanOrEqual(3)
112
+ })
113
+
114
+ it('assigns high confidence for small impacts', () => {
115
+ const graph = buildTestGraph([
116
+ ['A', 'B'],
117
+ ['B', 'nothing'],
118
+ ])
119
+ const analyzer = new ImpactAnalyzer(graph)
120
+ const result = analyzer.analyze(['fn:src/B.ts:B'])
121
+ expect(result.confidence).toBe('high')
122
+ })
123
+
124
+ it('does not include changed nodes in impacted', () => {
125
+ const graph = buildTestGraph([
126
+ ['A', 'B'],
127
+ ['B', 'nothing'],
128
+ ])
129
+ const analyzer = new ImpactAnalyzer(graph)
130
+ const result = analyzer.analyze(['fn:src/B.ts:B'])
131
+ expect(result.impacted).not.toContain('fn:src/B.ts:B')
132
+ expect(result.changed).toContain('fn:src/B.ts:B')
133
+ })
134
+ })
135
+
136
+ describe('ClusterDetector', () => {
137
+ it('groups files by directory', () => {
138
+ const files = [
139
+ mockParsedFile('src/auth/verify.ts',
140
+ [mockFunction('verifyToken', ['authMiddleware'], 'src/auth/verify.ts')],
141
+ [mockImport('./middleware', ['authMiddleware'], 'src/auth/middleware.ts')]
142
+ ),
143
+ mockParsedFile('src/auth/middleware.ts',
144
+ [mockFunction('authMiddleware', ['verifyToken'], 'src/auth/middleware.ts')],
145
+ [mockImport('./verify', ['verifyToken'], 'src/auth/verify.ts')]
146
+ ),
147
+ mockParsedFile('src/payments/charge.ts', [mockFunction('charge', [], 'src/payments/charge.ts')]),
148
+ ]
149
+ const graph = new GraphBuilder().build(files)
150
+ // Use minClusterSize=1 so even single-file groups appear
151
+ const detector = new ClusterDetector(graph, 1)
152
+ const clusters = detector.detect()
153
+ expect(clusters.length).toBeGreaterThanOrEqual(2)
154
+ const authCluster = clusters.find(c => c.id === 'auth')
155
+ expect(authCluster).toBeDefined()
156
+ expect(authCluster!.files).toHaveLength(2)
157
+ })
158
+
159
+ it('computes confidence scores', () => {
160
+ const files = [
161
+ mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
162
+ ]
163
+ const graph = new GraphBuilder().build(files)
164
+ const detector = new ClusterDetector(graph, 1)
165
+ const score = detector.computeClusterConfidence(['src/auth/verify.ts'])
166
+ expect(score).toBeGreaterThanOrEqual(0)
167
+ expect(score).toBeLessThanOrEqual(1)
168
+ })
169
+ })
@@ -1,49 +1,49 @@
1
- import { describe, it, expect } from 'bun:test'
2
- import { hashContent, hashFunctionBody } from '../src/hash/file-hasher'
3
- import { computeModuleHash, computeRootHash } from '../src/hash/tree-hasher'
4
-
5
- describe('hashContent', () => {
6
- it('produces consistent hashes', () => {
7
- expect(hashContent('hello')).toBe(hashContent('hello'))
8
- })
9
-
10
- it('produces different hashes for different content', () => {
11
- expect(hashContent('hello')).not.toBe(hashContent('world'))
12
- })
13
-
14
- it('returns 64 character hex string (SHA-256)', () => {
15
- const hash = hashContent('test')
16
- expect(hash.length).toBe(64)
17
- expect(hash).toMatch(/^[a-f0-9]{64}$/)
18
- })
19
- })
20
-
21
- describe('hashFunctionBody', () => {
22
- it('hashes specific line range', () => {
23
- const content = 'line1\nline2\nline3\nline4\nline5'
24
- const hash = hashFunctionBody(content, 2, 4)
25
- expect(hash).toBe(hashContent('line2\nline3\nline4'))
26
- })
27
- })
28
-
29
- describe('computeModuleHash', () => {
30
- it('produces consistent hash regardless of input order', () => {
31
- const hash1 = computeModuleHash(['abc', 'def', 'ghi'])
32
- const hash2 = computeModuleHash(['ghi', 'abc', 'def'])
33
- expect(hash1).toBe(hash2)
34
- })
35
-
36
- it('changes when any file hash changes', () => {
37
- const hash1 = computeModuleHash(['abc', 'def'])
38
- const hash2 = computeModuleHash(['abc', 'xyz'])
39
- expect(hash1).not.toBe(hash2)
40
- })
41
- })
42
-
43
- describe('computeRootHash', () => {
44
- it('produces consistent hash', () => {
45
- const hash1 = computeRootHash({ auth: 'abc', payments: 'def' })
46
- const hash2 = computeRootHash({ payments: 'def', auth: 'abc' })
47
- expect(hash1).toBe(hash2)
48
- })
49
- })
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { hashContent, hashFunctionBody } from '../src/hash/file-hasher'
3
+ import { computeModuleHash, computeRootHash } from '../src/hash/tree-hasher'
4
+
5
+ describe('hashContent', () => {
6
+ it('produces consistent hashes', () => {
7
+ expect(hashContent('hello')).toBe(hashContent('hello'))
8
+ })
9
+
10
+ it('produces different hashes for different content', () => {
11
+ expect(hashContent('hello')).not.toBe(hashContent('world'))
12
+ })
13
+
14
+ it('returns 64 character hex string (SHA-256)', () => {
15
+ const hash = hashContent('test')
16
+ expect(hash.length).toBe(64)
17
+ expect(hash).toMatch(/^[a-f0-9]{64}$/)
18
+ })
19
+ })
20
+
21
+ describe('hashFunctionBody', () => {
22
+ it('hashes specific line range', () => {
23
+ const content = 'line1\nline2\nline3\nline4\nline5'
24
+ const hash = hashFunctionBody(content, 2, 4)
25
+ expect(hash).toBe(hashContent('line2\nline3\nline4'))
26
+ })
27
+ })
28
+
29
+ describe('computeModuleHash', () => {
30
+ it('produces consistent hash regardless of input order', () => {
31
+ const hash1 = computeModuleHash(['abc', 'def', 'ghi'])
32
+ const hash2 = computeModuleHash(['ghi', 'abc', 'def'])
33
+ expect(hash1).toBe(hash2)
34
+ })
35
+
36
+ it('changes when any file hash changes', () => {
37
+ const hash1 = computeModuleHash(['abc', 'def'])
38
+ const hash2 = computeModuleHash(['abc', 'xyz'])
39
+ expect(hash1).not.toBe(hash2)
40
+ })
41
+ })
42
+
43
+ describe('computeRootHash', () => {
44
+ it('produces consistent hash', () => {
45
+ const hash1 = computeRootHash({ auth: 'abc', payments: 'def' })
46
+ const hash2 = computeRootHash({ payments: 'def', auth: 'abc' })
47
+ expect(hash1).toBe(hash2)
48
+ })
49
+ })
package/tests/helpers.ts CHANGED
@@ -1,83 +1,83 @@
1
- import { hashContent } from '../src/hash/file-hasher'
2
- import type { ParsedFile, ParsedFunction, ParsedImport } from '../src/parser/types'
3
- import { GraphBuilder } from '../src/graph/graph-builder'
4
- import type { DependencyGraph } from '../src/graph/types'
5
-
6
- /** Build a minimal ParsedFile for testing without actually parsing */
7
- export function mockParsedFile(
8
- filePath: string,
9
- functions: ParsedFunction[] = [],
10
- imports: ParsedImport[] = []
11
- ): ParsedFile {
12
- return {
13
- path: filePath,
14
- language: 'typescript',
15
- functions,
16
- classes: [],
17
- imports,
18
- exports: [],
19
- hash: hashContent(filePath),
20
- parsedAt: Date.now(),
21
- }
22
- }
23
-
24
- /** Build a minimal ParsedFunction */
25
- export function mockFunction(
26
- name: string,
27
- calls: string[] = [],
28
- file: string = 'src/test.ts',
29
- isExported: boolean = false
30
- ): ParsedFunction {
31
- return {
32
- id: `fn:${file}:${name}`,
33
- name,
34
- file,
35
- startLine: 1,
36
- endLine: 10,
37
- params: [],
38
- returnType: 'void',
39
- isExported,
40
- isAsync: false,
41
- calls,
42
- hash: hashContent(name),
43
- }
44
- }
45
-
46
- /** Build a minimal ParsedImport */
47
- export function mockImport(
48
- source: string,
49
- names: string[],
50
- resolvedPath: string = ''
51
- ): ParsedImport {
52
- return {
53
- source,
54
- resolvedPath,
55
- names,
56
- isDefault: false,
57
- isDynamic: false,
58
- }
59
- }
60
-
61
- /** Build a graph from simple tuple pairs for testing */
62
- export function buildTestGraph(
63
- callPairs: [string, string][]
64
- ): DependencyGraph {
65
- const fileNames = [...new Set(callPairs.flat().filter(x => x !== 'nothing'))]
66
- const parsedFiles = fileNames.map(name => {
67
- const targets = callPairs
68
- .filter(([from]) => from === name)
69
- .flatMap(([, to]) => to === 'nothing' ? [] : [to])
70
-
71
- // Build proper imports so GraphBuilder can resolve the call edges
72
- const imports = targets.map(to =>
73
- mockImport(`./${to}`, [to], `src/${to}.ts`)
74
- )
75
-
76
- return mockParsedFile(
77
- `src/${name}.ts`,
78
- [mockFunction(name, targets, `src/${name}.ts`)],
79
- imports
80
- )
81
- })
82
- return new GraphBuilder().build(parsedFiles)
83
- }
1
+ import { hashContent } from '../src/hash/file-hasher'
2
+ import type { ParsedFile, ParsedFunction, ParsedImport } from '../src/parser/types'
3
+ import { GraphBuilder } from '../src/graph/graph-builder'
4
+ import type { DependencyGraph } from '../src/graph/types'
5
+
6
+ /** Build a minimal ParsedFile for testing without actually parsing */
7
+ export function mockParsedFile(
8
+ filePath: string,
9
+ functions: ParsedFunction[] = [],
10
+ imports: ParsedImport[] = []
11
+ ): ParsedFile {
12
+ return {
13
+ path: filePath,
14
+ language: 'typescript',
15
+ functions,
16
+ classes: [],
17
+ imports,
18
+ exports: [],
19
+ hash: hashContent(filePath),
20
+ parsedAt: Date.now(),
21
+ }
22
+ }
23
+
24
+ /** Build a minimal ParsedFunction */
25
+ export function mockFunction(
26
+ name: string,
27
+ calls: string[] = [],
28
+ file: string = 'src/test.ts',
29
+ isExported: boolean = false
30
+ ): ParsedFunction {
31
+ return {
32
+ id: `fn:${file}:${name}`,
33
+ name,
34
+ file,
35
+ startLine: 1,
36
+ endLine: 10,
37
+ params: [],
38
+ returnType: 'void',
39
+ isExported,
40
+ isAsync: false,
41
+ calls,
42
+ hash: hashContent(name),
43
+ }
44
+ }
45
+
46
+ /** Build a minimal ParsedImport */
47
+ export function mockImport(
48
+ source: string,
49
+ names: string[],
50
+ resolvedPath: string = ''
51
+ ): ParsedImport {
52
+ return {
53
+ source,
54
+ resolvedPath,
55
+ names,
56
+ isDefault: false,
57
+ isDynamic: false,
58
+ }
59
+ }
60
+
61
+ /** Build a graph from simple tuple pairs for testing */
62
+ export function buildTestGraph(
63
+ callPairs: [string, string][]
64
+ ): DependencyGraph {
65
+ const fileNames = [...new Set(callPairs.flat().filter(x => x !== 'nothing'))]
66
+ const parsedFiles = fileNames.map(name => {
67
+ const targets = callPairs
68
+ .filter(([from]) => from === name)
69
+ .flatMap(([, to]) => to === 'nothing' ? [] : [to])
70
+
71
+ // Build proper imports so GraphBuilder can resolve the call edges
72
+ const imports = targets.map(to =>
73
+ mockImport(`./${to}`, [to], `src/${to}.ts`)
74
+ )
75
+
76
+ return mockParsedFile(
77
+ `src/${name}.ts`,
78
+ [mockFunction(name, targets, `src/${name}.ts`)],
79
+ imports
80
+ )
81
+ })
82
+ return new GraphBuilder().build(parsedFiles)
83
+ }