@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.
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,134 +1,134 @@
1
- import { describe, it, expect } from 'bun:test'
2
- import { MikkContractSchema, MikkLockSchema } from '../src/contract/schema'
3
- import { LockCompiler } from '../src/contract/lock-compiler'
4
- import { ContractGenerator } from '../src/contract/contract-generator'
5
- import { GraphBuilder } from '../src/graph/graph-builder'
6
- import { ClusterDetector } from '../src/graph/cluster-detector'
7
- import { mockParsedFile, mockFunction, mockImport } from './helpers'
8
-
9
- describe('MikkContractSchema', () => {
10
- const validContract = {
11
- version: '1.0.0',
12
- project: {
13
- name: 'my-api',
14
- description: 'REST API',
15
- language: 'typescript',
16
- entryPoints: ['src/index.ts'],
17
- },
18
- declared: {
19
- modules: [{
20
- id: 'auth',
21
- name: 'Authentication',
22
- description: 'Handles auth',
23
- paths: ['src/auth/**'],
24
- }],
25
- constraints: ['No direct DB access outside db/'],
26
- decisions: [{
27
- id: 'adr-001',
28
- title: 'JWT over sessions',
29
- reason: 'Scaling',
30
- date: '2024-01-15',
31
- }],
32
- },
33
- }
34
-
35
- it('validates correct mikk.json', () => {
36
- const result = MikkContractSchema.safeParse(validContract)
37
- expect(result.success).toBe(true)
38
- })
39
-
40
- it('rejects mikk.json with missing required fields', () => {
41
- const result = MikkContractSchema.safeParse({ version: '1.0.0' })
42
- expect(result.success).toBe(false)
43
- })
44
-
45
- it('applies default overwrite mode', () => {
46
- const result = MikkContractSchema.safeParse(validContract)
47
- expect(result.success).toBe(true)
48
- if (result.success) {
49
- expect(result.data.overwrite.mode).toBe('never')
50
- }
51
- })
52
- })
53
-
54
- describe('LockCompiler', () => {
55
- const compiler = new LockCompiler()
56
- const contract = MikkContractSchema.parse({
57
- version: '1.0.0',
58
- project: { name: 'test', description: 'Test', language: 'typescript', entryPoints: ['src/index.ts'] },
59
- declared: {
60
- modules: [
61
- { id: 'auth', name: 'Auth', description: 'Auth module', paths: ['src/auth/**'] },
62
- { id: 'utils', name: 'Utils', description: 'Utils', paths: ['src/utils/**'] },
63
- ],
64
- },
65
- })
66
-
67
- it('compiles a valid lock file', () => {
68
- const files = [
69
- mockParsedFile(
70
- 'src/auth/verify.ts',
71
- [mockFunction('verifyToken', ['jwtDecode'], 'src/auth/verify.ts', true)],
72
- [mockImport('../utils/jwt', ['jwtDecode'], 'src/utils/jwt.ts')]
73
- ),
74
- mockParsedFile('src/utils/jwt.ts', [mockFunction('jwtDecode', [], 'src/utils/jwt.ts', true)]),
75
- ]
76
- const graph = new GraphBuilder().build(files)
77
- const lock = compiler.compile(graph, contract, files)
78
-
79
- expect(lock.version).toBe('1.0.0')
80
- expect(lock.syncState.status).toBe('clean')
81
- expect(Object.keys(lock.functions).length).toBeGreaterThan(0)
82
- expect(Object.keys(lock.modules).length).toBeGreaterThanOrEqual(1)
83
- expect(Object.keys(lock.files).length).toBe(2)
84
- })
85
-
86
- it('assigns functions to correct modules', () => {
87
- const files = [
88
- mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
89
- ]
90
- const graph = new GraphBuilder().build(files)
91
- const lock = compiler.compile(graph, contract, files)
92
- expect(lock.functions['fn:src/auth/verify.ts:verifyToken']?.moduleId).toBe('auth')
93
- })
94
-
95
- it('computes stable module hash', () => {
96
- const files = [
97
- mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
98
- ]
99
- const graph = new GraphBuilder().build(files)
100
- const lock1 = compiler.compile(graph, contract, files)
101
- const lock2 = compiler.compile(graph, contract, files)
102
- expect(lock1.modules['auth']?.hash).toBe(lock2.modules['auth']?.hash)
103
- })
104
-
105
- it('validates against MikkLockSchema', () => {
106
- const files = [
107
- mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
108
- ]
109
- const graph = new GraphBuilder().build(files)
110
- const lock = compiler.compile(graph, contract, files)
111
- const result = MikkLockSchema.safeParse(lock)
112
- expect(result.success).toBe(true)
113
- })
114
- })
115
-
116
- describe('ContractGenerator', () => {
117
- it('generates contract from clusters', () => {
118
- const files = [
119
- mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts', true)]),
120
- mockParsedFile('src/auth/middleware.ts', [mockFunction('authMiddleware', [], 'src/auth/middleware.ts', true)]),
121
- ]
122
- const graph = new GraphBuilder().build(files)
123
- const detector = new ClusterDetector(graph, 1)
124
- const clusters = detector.detect()
125
-
126
- const generator = new ContractGenerator()
127
- const contract = generator.generateFromClusters(clusters, files, 'test-project')
128
-
129
- expect(contract.version).toBe('1.0.0')
130
- expect(contract.project.name).toBe('test-project')
131
- expect(contract.declared.modules.length).toBeGreaterThan(0)
132
- expect(contract.overwrite.mode).toBe('never')
133
- })
134
- })
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { MikkContractSchema, MikkLockSchema } from '../src/contract/schema'
3
+ import { LockCompiler } from '../src/contract/lock-compiler'
4
+ import { ContractGenerator } from '../src/contract/contract-generator'
5
+ import { GraphBuilder } from '../src/graph/graph-builder'
6
+ import { ClusterDetector } from '../src/graph/cluster-detector'
7
+ import { mockParsedFile, mockFunction, mockImport } from './helpers'
8
+
9
+ describe('MikkContractSchema', () => {
10
+ const validContract = {
11
+ version: '1.0.0',
12
+ project: {
13
+ name: 'my-api',
14
+ description: 'REST API',
15
+ language: 'typescript',
16
+ entryPoints: ['src/index.ts'],
17
+ },
18
+ declared: {
19
+ modules: [{
20
+ id: 'auth',
21
+ name: 'Authentication',
22
+ description: 'Handles auth',
23
+ paths: ['src/auth/**'],
24
+ }],
25
+ constraints: ['No direct DB access outside db/'],
26
+ decisions: [{
27
+ id: 'adr-001',
28
+ title: 'JWT over sessions',
29
+ reason: 'Scaling',
30
+ date: '2024-01-15',
31
+ }],
32
+ },
33
+ }
34
+
35
+ it('validates correct mikk.json', () => {
36
+ const result = MikkContractSchema.safeParse(validContract)
37
+ expect(result.success).toBe(true)
38
+ })
39
+
40
+ it('rejects mikk.json with missing required fields', () => {
41
+ const result = MikkContractSchema.safeParse({ version: '1.0.0' })
42
+ expect(result.success).toBe(false)
43
+ })
44
+
45
+ it('applies default overwrite mode', () => {
46
+ const result = MikkContractSchema.safeParse(validContract)
47
+ expect(result.success).toBe(true)
48
+ if (result.success) {
49
+ expect(result.data.overwrite.mode).toBe('never')
50
+ }
51
+ })
52
+ })
53
+
54
+ describe('LockCompiler', () => {
55
+ const compiler = new LockCompiler()
56
+ const contract = MikkContractSchema.parse({
57
+ version: '1.0.0',
58
+ project: { name: 'test', description: 'Test', language: 'typescript', entryPoints: ['src/index.ts'] },
59
+ declared: {
60
+ modules: [
61
+ { id: 'auth', name: 'Auth', description: 'Auth module', paths: ['src/auth/**'] },
62
+ { id: 'utils', name: 'Utils', description: 'Utils', paths: ['src/utils/**'] },
63
+ ],
64
+ },
65
+ })
66
+
67
+ it('compiles a valid lock file', () => {
68
+ const files = [
69
+ mockParsedFile(
70
+ 'src/auth/verify.ts',
71
+ [mockFunction('verifyToken', ['jwtDecode'], 'src/auth/verify.ts', true)],
72
+ [mockImport('../utils/jwt', ['jwtDecode'], 'src/utils/jwt.ts')]
73
+ ),
74
+ mockParsedFile('src/utils/jwt.ts', [mockFunction('jwtDecode', [], 'src/utils/jwt.ts', true)]),
75
+ ]
76
+ const graph = new GraphBuilder().build(files)
77
+ const lock = compiler.compile(graph, contract, files)
78
+
79
+ expect(lock.version).toBe('1.0.0')
80
+ expect(lock.syncState.status).toBe('clean')
81
+ expect(Object.keys(lock.functions).length).toBeGreaterThan(0)
82
+ expect(Object.keys(lock.modules).length).toBeGreaterThanOrEqual(1)
83
+ expect(Object.keys(lock.files).length).toBe(2)
84
+ })
85
+
86
+ it('assigns functions to correct modules', () => {
87
+ const files = [
88
+ mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
89
+ ]
90
+ const graph = new GraphBuilder().build(files)
91
+ const lock = compiler.compile(graph, contract, files)
92
+ expect(lock.functions['fn:src/auth/verify.ts:verifyToken']?.moduleId).toBe('auth')
93
+ })
94
+
95
+ it('computes stable module hash', () => {
96
+ const files = [
97
+ mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
98
+ ]
99
+ const graph = new GraphBuilder().build(files)
100
+ const lock1 = compiler.compile(graph, contract, files)
101
+ const lock2 = compiler.compile(graph, contract, files)
102
+ expect(lock1.modules['auth']?.hash).toBe(lock2.modules['auth']?.hash)
103
+ })
104
+
105
+ it('validates against MikkLockSchema', () => {
106
+ const files = [
107
+ mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts')]),
108
+ ]
109
+ const graph = new GraphBuilder().build(files)
110
+ const lock = compiler.compile(graph, contract, files)
111
+ const result = MikkLockSchema.safeParse(lock)
112
+ expect(result.success).toBe(true)
113
+ })
114
+ })
115
+
116
+ describe('ContractGenerator', () => {
117
+ it('generates contract from clusters', () => {
118
+ const files = [
119
+ mockParsedFile('src/auth/verify.ts', [mockFunction('verifyToken', [], 'src/auth/verify.ts', true)]),
120
+ mockParsedFile('src/auth/middleware.ts', [mockFunction('authMiddleware', [], 'src/auth/middleware.ts', true)]),
121
+ ]
122
+ const graph = new GraphBuilder().build(files)
123
+ const detector = new ClusterDetector(graph, 1)
124
+ const clusters = detector.detect()
125
+
126
+ const generator = new ContractGenerator()
127
+ const contract = generator.generateFromClusters(clusters, files, 'test-project')
128
+
129
+ expect(contract.version).toBe('1.0.0')
130
+ expect(contract.project.name).toBe('test-project')
131
+ expect(contract.declared.modules.length).toBeGreaterThan(0)
132
+ expect(contract.overwrite.mode).toBe('never')
133
+ })
134
+ })
@@ -1,5 +1,5 @@
1
- {
2
- "name": "simple-api",
3
- "private": true,
4
- "version": "1.0.0"
5
- }
1
+ {
2
+ "name": "simple-api",
3
+ "private": true,
4
+ "version": "1.0.0"
5
+ }
@@ -1,9 +1,9 @@
1
- import { verifyToken } from './verify'
2
-
3
- export function authMiddleware(req: any, res: any, next: any) {
4
- const token = req.headers.authorization
5
- if (!verifyToken(token)) {
6
- return res.status(401).json({ error: 'Unauthorized' })
7
- }
8
- next()
9
- }
1
+ import { verifyToken } from './verify'
2
+
3
+ export function authMiddleware(req: any, res: any, next: any) {
4
+ const token = req.headers.authorization
5
+ if (!verifyToken(token)) {
6
+ return res.status(401).json({ error: 'Unauthorized' })
7
+ }
8
+ next()
9
+ }
@@ -1,6 +1,6 @@
1
- import { jwtDecode } from '../utils/jwt'
2
-
3
- export function verifyToken(token: string): boolean {
4
- const decoded = jwtDecode(token)
5
- return decoded.exp > Date.now()
6
- }
1
+ import { jwtDecode } from '../utils/jwt'
2
+
3
+ export function verifyToken(token: string): boolean {
4
+ const decoded = jwtDecode(token)
5
+ return decoded.exp > Date.now()
6
+ }
@@ -1,9 +1,9 @@
1
- import { authMiddleware } from './auth/middleware'
2
-
3
- const app = {
4
- use: (handler: any) => { },
5
- listen: (port: number) => console.log(`Listening on ${port}`)
6
- }
7
-
8
- app.use(authMiddleware)
9
- app.listen(3000)
1
+ import { authMiddleware } from './auth/middleware'
2
+
3
+ const app = {
4
+ use: (handler: any) => { },
5
+ listen: (port: number) => console.log(`Listening on ${port}`)
6
+ }
7
+
8
+ app.use(authMiddleware)
9
+ app.listen(3000)
@@ -1,3 +1,3 @@
1
- export function jwtDecode(token: string): { exp: number } {
2
- return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
3
- }
1
+ export function jwtDecode(token: string): { exp: number } {
2
+ return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
3
+ }
@@ -1,8 +1,8 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true
7
- }
8
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true
7
+ }
8
+ }
@@ -1,142 +1,142 @@
1
- import { describe, test, expect } from 'bun:test'
2
- import { scoreFunctions, findFuzzyMatches, levenshtein, splitCamelCase, extractKeywords } from '../src/utils/fuzzy-match'
3
- import type { MikkLock } from '../src/contract/schema'
4
-
5
- const mockLock: MikkLock = {
6
- version: '1.0.0',
7
- generatedAt: new Date().toISOString(),
8
- generatorVersion: '1.1.0',
9
- projectRoot: '/test',
10
- syncState: { status: 'clean', lastSyncAt: new Date().toISOString(), lockHash: 'abc', contractHash: 'def' },
11
- modules: {},
12
- functions: {
13
- 'fn:auth:verifyToken': {
14
- id: 'fn:auth:verifyToken', name: 'verifyToken', file: 'src/auth/verify.ts',
15
- startLine: 1, endLine: 10, hash: 'h1', calls: [], calledBy: ['fn:api:handleLogin'],
16
- moduleId: 'auth',
17
- },
18
- 'fn:auth:refreshToken': {
19
- id: 'fn:auth:refreshToken', name: 'refreshToken', file: 'src/auth/refresh.ts',
20
- startLine: 1, endLine: 15, hash: 'h2', calls: [], calledBy: [],
21
- moduleId: 'auth',
22
- },
23
- 'fn:api:handleLogin': {
24
- id: 'fn:api:handleLogin', name: 'handleLogin', file: 'src/api/login.ts',
25
- startLine: 1, endLine: 20, hash: 'h3', calls: ['fn:auth:verifyToken'], calledBy: [],
26
- moduleId: 'api',
27
- },
28
- 'fn:db:queryUsers': {
29
- id: 'fn:db:queryUsers', name: 'queryUsers', file: 'src/db/users.ts',
30
- startLine: 1, endLine: 8, hash: 'h4', calls: [], calledBy: [],
31
- moduleId: 'db',
32
- },
33
- 'fn:api:validateUserInput': {
34
- id: 'fn:api:validateUserInput', name: 'validateUserInput', file: 'src/api/validation.ts',
35
- startLine: 1, endLine: 12, hash: 'h5', calls: [], calledBy: [],
36
- moduleId: 'api',
37
- },
38
- },
39
- files: {},
40
- graph: { nodes: 5, edges: 1, rootHash: 'root' },
41
- }
42
-
43
- describe('levenshtein', () => {
44
- test('identical strings → 0', () => {
45
- expect(levenshtein('hello', 'hello')).toBe(0)
46
- })
47
-
48
- test('empty vs non-empty', () => {
49
- expect(levenshtein('', 'abc')).toBe(3)
50
- expect(levenshtein('abc', '')).toBe(3)
51
- })
52
-
53
- test('single char difference', () => {
54
- expect(levenshtein('cat', 'bat')).toBe(1)
55
- })
56
-
57
- test('completely different strings', () => {
58
- expect(levenshtein('abc', 'xyz')).toBe(3)
59
- })
60
-
61
- test('insertion', () => {
62
- expect(levenshtein('test', 'tests')).toBe(1)
63
- })
64
- })
65
-
66
- describe('splitCamelCase', () => {
67
- test('camelCase', () => {
68
- expect(splitCamelCase('verifyToken')).toEqual(['verify', 'Token'])
69
- })
70
-
71
- test('PascalCase', () => {
72
- expect(splitCamelCase('VerifyToken')).toEqual(['Verify', 'Token'])
73
- })
74
-
75
- test('snake_case', () => {
76
- expect(splitCamelCase('verify_token')).toEqual(['verify', 'token'])
77
- })
78
-
79
- test('multiple words', () => {
80
- expect(splitCamelCase('validateUserInput')).toEqual(['validate', 'User', 'Input'])
81
- })
82
- })
83
-
84
- describe('extractKeywords', () => {
85
- test('removes stop words', () => {
86
- const keywords = extractKeywords('fix the auth token bug')
87
- expect(keywords).toContain('auth')
88
- expect(keywords).toContain('token')
89
- expect(keywords).not.toContain('the')
90
- })
91
-
92
- test('lowercases everything', () => {
93
- const keywords = extractKeywords('Token Validation')
94
- expect(keywords).toEqual(['token', 'validation'])
95
- })
96
- })
97
-
98
- describe('scoreFunctions', () => {
99
- test('exact name match scores highest', () => {
100
- const results = scoreFunctions('fix verifyToken', mockLock, 5)
101
- expect(results.length).toBeGreaterThan(0)
102
- expect(results[0].name).toBe('verifyToken')
103
- })
104
-
105
- test('keyword match works', () => {
106
- const results = scoreFunctions('fix the token issue', mockLock, 5)
107
- const names = results.map(r => r.name)
108
- // Should find token-related functions
109
- expect(names).toContain('verifyToken')
110
- expect(names).toContain('refreshToken')
111
- })
112
-
113
- test('module match boosts score', () => {
114
- const results = scoreFunctions('auth problem', mockLock, 5)
115
- // Auth module functions should appear
116
- const authFns = results.filter(r => r.moduleId === 'auth')
117
- expect(authFns.length).toBeGreaterThan(0)
118
- })
119
-
120
- test('maxResults limits output', () => {
121
- const results = scoreFunctions('function', mockLock, 2)
122
- expect(results.length).toBeLessThanOrEqual(2)
123
- })
124
- })
125
-
126
- describe('findFuzzyMatches', () => {
127
- test('finds similar names', () => {
128
- const matches = findFuzzyMatches('verifyTokens', mockLock, 3)
129
- expect(matches).toContain('verifyToken')
130
- })
131
-
132
- test('finds substring matches', () => {
133
- const matches = findFuzzyMatches('Token', mockLock, 5)
134
- expect(matches).toContain('verifyToken')
135
- expect(matches).toContain('refreshToken')
136
- })
137
-
138
- test('returns empty for very dissimilar terms', () => {
139
- const matches = findFuzzyMatches('xyzzyplugh', mockLock, 5)
140
- expect(matches.length).toBe(0)
141
- })
142
- })
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { scoreFunctions, findFuzzyMatches, levenshtein, splitCamelCase, extractKeywords } from '../src/utils/fuzzy-match'
3
+ import type { MikkLock } from '../src/contract/schema'
4
+
5
+ const mockLock: MikkLock = {
6
+ version: '1.0.0',
7
+ generatedAt: new Date().toISOString(),
8
+ generatorVersion: '1.1.0',
9
+ projectRoot: '/test',
10
+ syncState: { status: 'clean', lastSyncAt: new Date().toISOString(), lockHash: 'abc', contractHash: 'def' },
11
+ modules: {},
12
+ functions: {
13
+ 'fn:auth:verifyToken': {
14
+ id: 'fn:auth:verifyToken', name: 'verifyToken', file: 'src/auth/verify.ts',
15
+ startLine: 1, endLine: 10, hash: 'h1', calls: [], calledBy: ['fn:api:handleLogin'],
16
+ moduleId: 'auth',
17
+ },
18
+ 'fn:auth:refreshToken': {
19
+ id: 'fn:auth:refreshToken', name: 'refreshToken', file: 'src/auth/refresh.ts',
20
+ startLine: 1, endLine: 15, hash: 'h2', calls: [], calledBy: [],
21
+ moduleId: 'auth',
22
+ },
23
+ 'fn:api:handleLogin': {
24
+ id: 'fn:api:handleLogin', name: 'handleLogin', file: 'src/api/login.ts',
25
+ startLine: 1, endLine: 20, hash: 'h3', calls: ['fn:auth:verifyToken'], calledBy: [],
26
+ moduleId: 'api',
27
+ },
28
+ 'fn:db:queryUsers': {
29
+ id: 'fn:db:queryUsers', name: 'queryUsers', file: 'src/db/users.ts',
30
+ startLine: 1, endLine: 8, hash: 'h4', calls: [], calledBy: [],
31
+ moduleId: 'db',
32
+ },
33
+ 'fn:api:validateUserInput': {
34
+ id: 'fn:api:validateUserInput', name: 'validateUserInput', file: 'src/api/validation.ts',
35
+ startLine: 1, endLine: 12, hash: 'h5', calls: [], calledBy: [],
36
+ moduleId: 'api',
37
+ },
38
+ },
39
+ files: {},
40
+ graph: { nodes: 5, edges: 1, rootHash: 'root' },
41
+ }
42
+
43
+ describe('levenshtein', () => {
44
+ test('identical strings → 0', () => {
45
+ expect(levenshtein('hello', 'hello')).toBe(0)
46
+ })
47
+
48
+ test('empty vs non-empty', () => {
49
+ expect(levenshtein('', 'abc')).toBe(3)
50
+ expect(levenshtein('abc', '')).toBe(3)
51
+ })
52
+
53
+ test('single char difference', () => {
54
+ expect(levenshtein('cat', 'bat')).toBe(1)
55
+ })
56
+
57
+ test('completely different strings', () => {
58
+ expect(levenshtein('abc', 'xyz')).toBe(3)
59
+ })
60
+
61
+ test('insertion', () => {
62
+ expect(levenshtein('test', 'tests')).toBe(1)
63
+ })
64
+ })
65
+
66
+ describe('splitCamelCase', () => {
67
+ test('camelCase', () => {
68
+ expect(splitCamelCase('verifyToken')).toEqual(['verify', 'Token'])
69
+ })
70
+
71
+ test('PascalCase', () => {
72
+ expect(splitCamelCase('VerifyToken')).toEqual(['Verify', 'Token'])
73
+ })
74
+
75
+ test('snake_case', () => {
76
+ expect(splitCamelCase('verify_token')).toEqual(['verify', 'token'])
77
+ })
78
+
79
+ test('multiple words', () => {
80
+ expect(splitCamelCase('validateUserInput')).toEqual(['validate', 'User', 'Input'])
81
+ })
82
+ })
83
+
84
+ describe('extractKeywords', () => {
85
+ test('removes stop words', () => {
86
+ const keywords = extractKeywords('fix the auth token bug')
87
+ expect(keywords).toContain('auth')
88
+ expect(keywords).toContain('token')
89
+ expect(keywords).not.toContain('the')
90
+ })
91
+
92
+ test('lowercases everything', () => {
93
+ const keywords = extractKeywords('Token Validation')
94
+ expect(keywords).toEqual(['token', 'validation'])
95
+ })
96
+ })
97
+
98
+ describe('scoreFunctions', () => {
99
+ test('exact name match scores highest', () => {
100
+ const results = scoreFunctions('fix verifyToken', mockLock, 5)
101
+ expect(results.length).toBeGreaterThan(0)
102
+ expect(results[0].name).toBe('verifyToken')
103
+ })
104
+
105
+ test('keyword match works', () => {
106
+ const results = scoreFunctions('fix the token issue', mockLock, 5)
107
+ const names = results.map(r => r.name)
108
+ // Should find token-related functions
109
+ expect(names).toContain('verifyToken')
110
+ expect(names).toContain('refreshToken')
111
+ })
112
+
113
+ test('module match boosts score', () => {
114
+ const results = scoreFunctions('auth problem', mockLock, 5)
115
+ // Auth module functions should appear
116
+ const authFns = results.filter(r => r.moduleId === 'auth')
117
+ expect(authFns.length).toBeGreaterThan(0)
118
+ })
119
+
120
+ test('maxResults limits output', () => {
121
+ const results = scoreFunctions('function', mockLock, 2)
122
+ expect(results.length).toBeLessThanOrEqual(2)
123
+ })
124
+ })
125
+
126
+ describe('findFuzzyMatches', () => {
127
+ test('finds similar names', () => {
128
+ const matches = findFuzzyMatches('verifyTokens', mockLock, 3)
129
+ expect(matches).toContain('verifyToken')
130
+ })
131
+
132
+ test('finds substring matches', () => {
133
+ const matches = findFuzzyMatches('Token', mockLock, 5)
134
+ expect(matches).toContain('verifyToken')
135
+ expect(matches).toContain('refreshToken')
136
+ })
137
+
138
+ test('returns empty for very dissimilar terms', () => {
139
+ const matches = findFuzzyMatches('xyzzyplugh', mockLock, 5)
140
+ expect(matches.length).toBe(0)
141
+ })
142
+ })