@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
package/tests/contract.test.ts
CHANGED
|
@@ -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
|
+
})
|