@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,186 +1,186 @@
1
- import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
2
-
3
- /**
4
- * FuzzyMatcher — scores lock file functions against a search term or prompt.
5
- *
6
- * Used for:
7
- * - "Did you mean?" suggestions when a function name isn't found
8
- * - Ranking functions by relevance to a developer's prompt
9
- * - Seed selection for context generation
10
- *
11
- * Per spec Section 6: scoring uses exact match, keyword overlap,
12
- * camelCase decomposition, and Levenshtein distance.
13
- */
14
-
15
- // ── Public API ───────────────────────────────────────────────
16
-
17
- export interface FuzzyMatch {
18
- name: string
19
- file: string
20
- moduleId: string
21
- score: number
22
- }
23
-
24
- /**
25
- * Score every function in the lock against a prompt and return
26
- * the top matches sorted by relevance.
27
- */
28
- export function scoreFunctions(
29
- prompt: string,
30
- lock: MikkLock,
31
- maxResults = 10
32
- ): FuzzyMatch[] {
33
- const keywords = extractKeywords(prompt)
34
- const promptLower = prompt.toLowerCase()
35
- const results: FuzzyMatch[] = []
36
-
37
- for (const fn of Object.values(lock.functions)) {
38
- const score = scoreSingleFunction(fn, promptLower, keywords)
39
- if (score > 0.2) {
40
- results.push({
41
- name: fn.name,
42
- file: fn.file,
43
- moduleId: fn.moduleId,
44
- score: Math.min(score, 1.0),
45
- })
46
- }
47
- }
48
-
49
- return results
50
- .sort((a, b) => b.score - a.score)
51
- .slice(0, maxResults)
52
- }
53
-
54
- /**
55
- * Find functions whose names are similar to `searchTerm` — for
56
- * "Did you mean?" suggestions when an exact match is not found.
57
- */
58
- export function findFuzzyMatches(
59
- searchTerm: string,
60
- lock: MikkLock,
61
- maxResults = 5
62
- ): string[] {
63
- const searchLower = searchTerm.toLowerCase()
64
- const scored: { name: string; score: number }[] = []
65
-
66
- for (const fn of Object.values(lock.functions)) {
67
- const nameLower = fn.name.toLowerCase()
68
-
69
- // Levenshtein distance normalized by length
70
- const distance = levenshtein(searchLower, nameLower)
71
- const maxLen = Math.max(searchLower.length, nameLower.length)
72
- const similarity = 1 - (distance / maxLen)
73
-
74
- // Substring containment bonus
75
- const containsScore =
76
- nameLower.includes(searchLower) || searchLower.includes(nameLower)
77
- ? 0.3
78
- : 0
79
-
80
- const totalScore = similarity + containsScore
81
-
82
- if (totalScore > 0.5) {
83
- scored.push({ name: fn.name, score: totalScore })
84
- }
85
- }
86
-
87
- return scored
88
- .sort((a, b) => b.score - a.score)
89
- .slice(0, maxResults)
90
- .map(s => s.name)
91
- }
92
-
93
- // ── Scoring ──────────────────────────────────────────────────
94
-
95
- function scoreSingleFunction(
96
- fn: MikkLockFunction,
97
- promptLower: string,
98
- keywords: string[]
99
- ): number {
100
- let score = 0
101
- const fnNameLower = fn.name.toLowerCase()
102
- const fileLower = fn.file.toLowerCase()
103
-
104
- // Exact name match in prompt → very high
105
- if (promptLower.includes(fnNameLower) && fnNameLower.length > 3) {
106
- score += 0.9
107
- }
108
-
109
- // Keyword → function name matches
110
- for (const kw of keywords) {
111
- if (fnNameLower.includes(kw)) score += 0.3
112
- if (fileLower.includes(kw)) score += 0.15
113
- }
114
-
115
- // CamelCase word partial matches
116
- const fnWords = splitCamelCase(fn.name).map(w => w.toLowerCase())
117
- for (const kw of keywords) {
118
- if (fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
119
- score += 0.2
120
- }
121
- }
122
-
123
- // Module match — "fix auth bug" → functions in auth module score higher
124
- for (const kw of keywords) {
125
- if (fn.moduleId.toLowerCase().includes(kw)) score += 0.25
126
- }
127
-
128
- return score
129
- }
130
-
131
- // ── Levenshtein Distance ─────────────────────────────────────
132
-
133
- /**
134
- * Standard Levenshtein edit distance. O(n*m) time, O(min(n,m)) space.
135
- */
136
- export function levenshtein(a: string, b: string): number {
137
- if (a.length === 0) return b.length
138
- if (b.length === 0) return a.length
139
-
140
- // Ensure a is the shorter string for space efficiency
141
- if (a.length > b.length) [a, b] = [b, a]
142
-
143
- let prev = Array.from({ length: a.length + 1 }, (_, i) => i)
144
- let curr = new Array<number>(a.length + 1)
145
-
146
- for (let j = 1; j <= b.length; j++) {
147
- curr[0] = j
148
- for (let i = 1; i <= a.length; i++) {
149
- const cost = a[i - 1] === b[j - 1] ? 0 : 1
150
- curr[i] = Math.min(
151
- prev[i] + 1, // deletion
152
- curr[i - 1] + 1, // insertion
153
- prev[i - 1] + cost // substitution
154
- )
155
- }
156
- ;[prev, curr] = [curr, prev]
157
- }
158
-
159
- return prev[a.length]
160
- }
161
-
162
- // ── Helpers ──────────────────────────────────────────────────
163
-
164
- export function splitCamelCase(name: string): string[] {
165
- return name
166
- .replace(/([a-z])([A-Z])/g, '$1 $2')
167
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
168
- .split(/[\s_-]+/)
169
- .filter(w => w.length > 0)
170
- }
171
-
172
- export function extractKeywords(text: string): string[] {
173
- const stopWords = new Set([
174
- 'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or',
175
- 'is', 'it', 'fix', 'add', 'update', 'change', 'modify', 'make', 'create',
176
- 'bug', 'issue', 'error', 'problem', 'feature', 'function', 'file', 'code',
177
- 'new', 'old', 'all', 'this', 'that', 'from', 'with', 'move', 'remove',
178
- 'delete', 'refactor', 'should', 'can', 'will', 'must', 'need', 'want',
179
- ])
180
-
181
- return text
182
- .toLowerCase()
183
- .replace(/[^a-z0-9\s]/g, ' ')
184
- .split(/\s+/)
185
- .filter(w => w.length > 2 && !stopWords.has(w))
186
- }
1
+ import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
2
+
3
+ /**
4
+ * FuzzyMatcher — scores lock file functions against a search term or prompt.
5
+ *
6
+ * Used for:
7
+ * - "Did you mean?" suggestions when a function name isn't found
8
+ * - Ranking functions by relevance to a developer's prompt
9
+ * - Seed selection for context generation
10
+ *
11
+ * Per spec Section 6: scoring uses exact match, keyword overlap,
12
+ * camelCase decomposition, and Levenshtein distance.
13
+ */
14
+
15
+ // ── Public API ───────────────────────────────────────────────
16
+
17
+ export interface FuzzyMatch {
18
+ name: string
19
+ file: string
20
+ moduleId: string
21
+ score: number
22
+ }
23
+
24
+ /**
25
+ * Score every function in the lock against a prompt and return
26
+ * the top matches sorted by relevance.
27
+ */
28
+ export function scoreFunctions(
29
+ prompt: string,
30
+ lock: MikkLock,
31
+ maxResults = 10
32
+ ): FuzzyMatch[] {
33
+ const keywords = extractKeywords(prompt)
34
+ const promptLower = prompt.toLowerCase()
35
+ const results: FuzzyMatch[] = []
36
+
37
+ for (const fn of Object.values(lock.functions)) {
38
+ const score = scoreSingleFunction(fn, promptLower, keywords)
39
+ if (score > 0.2) {
40
+ results.push({
41
+ name: fn.name,
42
+ file: fn.file,
43
+ moduleId: fn.moduleId,
44
+ score: Math.min(score, 1.0),
45
+ })
46
+ }
47
+ }
48
+
49
+ return results
50
+ .sort((a, b) => b.score - a.score)
51
+ .slice(0, maxResults)
52
+ }
53
+
54
+ /**
55
+ * Find functions whose names are similar to `searchTerm` — for
56
+ * "Did you mean?" suggestions when an exact match is not found.
57
+ */
58
+ export function findFuzzyMatches(
59
+ searchTerm: string,
60
+ lock: MikkLock,
61
+ maxResults = 5
62
+ ): string[] {
63
+ const searchLower = searchTerm.toLowerCase()
64
+ const scored: { name: string; score: number }[] = []
65
+
66
+ for (const fn of Object.values(lock.functions)) {
67
+ const nameLower = fn.name.toLowerCase()
68
+
69
+ // Levenshtein distance normalized by length
70
+ const distance = levenshtein(searchLower, nameLower)
71
+ const maxLen = Math.max(searchLower.length, nameLower.length)
72
+ const similarity = 1 - (distance / maxLen)
73
+
74
+ // Substring containment bonus
75
+ const containsScore =
76
+ nameLower.includes(searchLower) || searchLower.includes(nameLower)
77
+ ? 0.3
78
+ : 0
79
+
80
+ const totalScore = similarity + containsScore
81
+
82
+ if (totalScore > 0.5) {
83
+ scored.push({ name: fn.name, score: totalScore })
84
+ }
85
+ }
86
+
87
+ return scored
88
+ .sort((a, b) => b.score - a.score)
89
+ .slice(0, maxResults)
90
+ .map(s => s.name)
91
+ }
92
+
93
+ // ── Scoring ──────────────────────────────────────────────────
94
+
95
+ function scoreSingleFunction(
96
+ fn: MikkLockFunction,
97
+ promptLower: string,
98
+ keywords: string[]
99
+ ): number {
100
+ let score = 0
101
+ const fnNameLower = fn.name.toLowerCase()
102
+ const fileLower = fn.file.toLowerCase()
103
+
104
+ // Exact name match in prompt → very high
105
+ if (promptLower.includes(fnNameLower) && fnNameLower.length > 3) {
106
+ score += 0.9
107
+ }
108
+
109
+ // Keyword → function name matches
110
+ for (const kw of keywords) {
111
+ if (fnNameLower.includes(kw)) score += 0.3
112
+ if (fileLower.includes(kw)) score += 0.15
113
+ }
114
+
115
+ // CamelCase word partial matches
116
+ const fnWords = splitCamelCase(fn.name).map(w => w.toLowerCase())
117
+ for (const kw of keywords) {
118
+ if (fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
119
+ score += 0.2
120
+ }
121
+ }
122
+
123
+ // Module match — "fix auth bug" → functions in auth module score higher
124
+ for (const kw of keywords) {
125
+ if (fn.moduleId.toLowerCase().includes(kw)) score += 0.25
126
+ }
127
+
128
+ return score
129
+ }
130
+
131
+ // ── Levenshtein Distance ─────────────────────────────────────
132
+
133
+ /**
134
+ * Standard Levenshtein edit distance. O(n*m) time, O(min(n,m)) space.
135
+ */
136
+ export function levenshtein(a: string, b: string): number {
137
+ if (a.length === 0) return b.length
138
+ if (b.length === 0) return a.length
139
+
140
+ // Ensure a is the shorter string for space efficiency
141
+ if (a.length > b.length) [a, b] = [b, a]
142
+
143
+ let prev = Array.from({ length: a.length + 1 }, (_, i) => i)
144
+ let curr = new Array<number>(a.length + 1)
145
+
146
+ for (let j = 1; j <= b.length; j++) {
147
+ curr[0] = j
148
+ for (let i = 1; i <= a.length; i++) {
149
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
150
+ curr[i] = Math.min(
151
+ prev[i] + 1, // deletion
152
+ curr[i - 1] + 1, // insertion
153
+ prev[i - 1] + cost // substitution
154
+ )
155
+ }
156
+ ;[prev, curr] = [curr, prev]
157
+ }
158
+
159
+ return prev[a.length]
160
+ }
161
+
162
+ // ── Helpers ──────────────────────────────────────────────────
163
+
164
+ export function splitCamelCase(name: string): string[] {
165
+ return name
166
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
167
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
168
+ .split(/[\s_-]+/)
169
+ .filter(w => w.length > 0)
170
+ }
171
+
172
+ export function extractKeywords(text: string): string[] {
173
+ const stopWords = new Set([
174
+ 'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or',
175
+ 'is', 'it', 'fix', 'add', 'update', 'change', 'modify', 'make', 'create',
176
+ 'bug', 'issue', 'error', 'problem', 'feature', 'function', 'file', 'code',
177
+ 'new', 'old', 'all', 'this', 'that', 'from', 'with', 'move', 'remove',
178
+ 'delete', 'refactor', 'should', 'can', 'will', 'must', 'need', 'want',
179
+ ])
180
+
181
+ return text
182
+ .toLowerCase()
183
+ .replace(/[^a-z0-9\s]/g, ' ')
184
+ .split(/\s+/)
185
+ .filter(w => w.length > 2 && !stopWords.has(w))
186
+ }
@@ -1,36 +1,36 @@
1
- type LogLevel = 'debug' | 'info' | 'warn' | 'error'
2
-
3
- let currentLogLevel: LogLevel = 'info'
4
- const levelOrder: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 }
5
-
6
- export function setLogLevel(level: LogLevel | 'silent') {
7
- if (level === 'silent') {
8
- currentLogLevel = 'error'
9
- process.env.MIKK_LOG_LEVEL = 'silent'
10
- } else {
11
- currentLogLevel = level
12
- }
13
- }
14
-
15
- function shouldLog(level: LogLevel): boolean {
16
- if (process.env.MIKK_LOG_LEVEL === 'silent') return false
17
- return levelOrder[level] >= levelOrder[currentLogLevel]
18
- }
19
-
20
- function log(level: LogLevel, message: string, data?: object) {
21
- if (!shouldLog(level)) return
22
- const entry = {
23
- level,
24
- timestamp: new Date().toISOString(),
25
- message,
26
- ...data
27
- }
28
- console.error(JSON.stringify(entry))
29
- }
30
-
31
- export const logger = {
32
- debug: (message: string, data?: object) => log('debug', message, data),
33
- info: (message: string, data?: object) => log('info', message, data),
34
- warn: (message: string, data?: object) => log('warn', message, data),
35
- error: (message: string, data?: object) => log('error', message, data),
36
- }
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error'
2
+
3
+ let currentLogLevel: LogLevel = 'info'
4
+ const levelOrder: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 }
5
+
6
+ export function setLogLevel(level: LogLevel | 'silent') {
7
+ if (level === 'silent') {
8
+ currentLogLevel = 'error'
9
+ process.env.MIKK_LOG_LEVEL = 'silent'
10
+ } else {
11
+ currentLogLevel = level
12
+ }
13
+ }
14
+
15
+ function shouldLog(level: LogLevel): boolean {
16
+ if (process.env.MIKK_LOG_LEVEL === 'silent') return false
17
+ return levelOrder[level] >= levelOrder[currentLogLevel]
18
+ }
19
+
20
+ function log(level: LogLevel, message: string, data?: object) {
21
+ if (!shouldLog(level)) return
22
+ const entry = {
23
+ level,
24
+ timestamp: new Date().toISOString(),
25
+ message,
26
+ ...data
27
+ }
28
+ console.error(JSON.stringify(entry))
29
+ }
30
+
31
+ export const logger = {
32
+ debug: (message: string, data?: object) => log('debug', message, data),
33
+ info: (message: string, data?: object) => log('info', message, data),
34
+ warn: (message: string, data?: object) => log('warn', message, data),
35
+ error: (message: string, data?: object) => log('error', message, data),
36
+ }
@@ -1,19 +1,19 @@
1
- /**
2
- * Simple minimatch-like glob matching utility.
3
- * Supports ** (any depth directory) and * (wildcard) patterns.
4
- */
5
- export function minimatch(filePath: string, pattern: string): boolean {
6
- // Normalize both to forward slashes
7
- const normalizedPath = filePath.replace(/\\/g, '/')
8
- const normalizedPattern = pattern.replace(/\\/g, '/')
9
-
10
- // Convert glob pattern to regex
11
- const regexStr = normalizedPattern
12
- .replace(/\./g, '\\.')
13
- .replace(/\*\*\//g, '(?:.+/)?')
14
- .replace(/\*\*/g, '.*')
15
- .replace(/\*/g, '[^/]*')
16
-
17
- const regex = new RegExp(`^${regexStr}$`)
18
- return regex.test(normalizedPath)
19
- }
1
+ /**
2
+ * Simple minimatch-like glob matching utility.
3
+ * Supports ** (any depth directory) and * (wildcard) patterns.
4
+ */
5
+ export function minimatch(filePath: string, pattern: string): boolean {
6
+ // Normalize both to forward slashes
7
+ const normalizedPath = filePath.replace(/\\/g, '/')
8
+ const normalizedPattern = pattern.replace(/\\/g, '/')
9
+
10
+ // Convert glob pattern to regex
11
+ const regexStr = normalizedPattern
12
+ .replace(/\./g, '\\.')
13
+ .replace(/\*\*\//g, '(?:.+/)?')
14
+ .replace(/\*\*/g, '.*')
15
+ .replace(/\*/g, '[^/]*')
16
+
17
+ const regex = new RegExp(`^${regexStr}$`)
18
+ return regex.test(normalizedPath)
19
+ }