@getmikk/core 2.0.14 → 2.0.15

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 (64) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/src/analysis/type-flow.ts +1 -1
  4. package/src/cache/incremental-cache.ts +86 -80
  5. package/src/contract/contract-reader.ts +1 -0
  6. package/src/contract/lock-compiler.ts +95 -13
  7. package/src/contract/schema.ts +2 -0
  8. package/src/error-handler.ts +2 -1
  9. package/src/graph/cluster-detector.ts +2 -4
  10. package/src/graph/dead-code-detector.ts +303 -117
  11. package/src/graph/graph-builder.ts +21 -161
  12. package/src/graph/impact-analyzer.ts +1 -0
  13. package/src/graph/index.ts +2 -0
  14. package/src/graph/rich-function-index.ts +1080 -0
  15. package/src/graph/symbol-table.ts +252 -0
  16. package/src/hash/hash-store.ts +1 -0
  17. package/src/index.ts +2 -0
  18. package/src/parser/base-extractor.ts +19 -0
  19. package/src/parser/boundary-checker.ts +31 -12
  20. package/src/parser/error-recovery.ts +5 -4
  21. package/src/parser/function-body-extractor.ts +248 -0
  22. package/src/parser/go/go-extractor.ts +249 -676
  23. package/src/parser/index.ts +132 -318
  24. package/src/parser/language-registry.ts +57 -0
  25. package/src/parser/oxc-parser.ts +166 -28
  26. package/src/parser/oxc-resolver.ts +179 -11
  27. package/src/parser/parser-constants.ts +1 -0
  28. package/src/parser/rust/rust-extractor.ts +109 -0
  29. package/src/parser/tree-sitter/parser.ts +369 -62
  30. package/src/parser/tree-sitter/queries.ts +106 -10
  31. package/src/parser/types.ts +20 -1
  32. package/src/search/bm25.ts +21 -8
  33. package/src/search/direct-search.ts +472 -0
  34. package/src/search/embedding-provider.ts +249 -0
  35. package/src/search/index.ts +12 -0
  36. package/src/search/semantic-search.ts +435 -0
  37. package/src/utils/artifact-transaction.ts +1 -0
  38. package/src/utils/atomic-write.ts +1 -0
  39. package/src/utils/errors.ts +89 -4
  40. package/src/utils/fs.ts +104 -50
  41. package/src/utils/json.ts +1 -0
  42. package/src/utils/language-registry.ts +84 -6
  43. package/src/utils/path.ts +26 -0
  44. package/tests/dead-code.test.ts +3 -2
  45. package/tests/direct-search.test.ts +435 -0
  46. package/tests/error-recovery.test.ts +143 -0
  47. package/tests/fixtures/simple-api/src/index.ts +1 -1
  48. package/tests/go-parser.test.ts +19 -335
  49. package/tests/js-parser.test.ts +18 -1089
  50. package/tests/language-registry-all.test.ts +276 -0
  51. package/tests/language-registry.test.ts +6 -4
  52. package/tests/parse-diagnostics.test.ts +9 -96
  53. package/tests/parser.test.ts +42 -771
  54. package/tests/polyglot-parser.test.ts +117 -0
  55. package/tests/rich-function-index.test.ts +703 -0
  56. package/tests/tree-sitter-parser.test.ts +108 -80
  57. package/tests/ts-parser.test.ts +8 -8
  58. package/tests/verification.test.ts +175 -0
  59. package/src/parser/base-parser.ts +0 -16
  60. package/src/parser/go/go-parser.ts +0 -43
  61. package/src/parser/javascript/js-extractor.ts +0 -278
  62. package/src/parser/javascript/js-parser.ts +0 -101
  63. package/src/parser/typescript/ts-extractor.ts +0 -447
  64. package/src/parser/typescript/ts-parser.ts +0 -36
@@ -2,12 +2,25 @@ export class MikkError extends Error {
2
2
  constructor(message: string, public code: string) {
3
3
  super(message)
4
4
  this.name = 'MikkError'
5
+ Error.captureStackTrace?.(this, this.constructor)
6
+ }
7
+
8
+ toJSON() {
9
+ return {
10
+ name: this.name,
11
+ message: this.message,
12
+ code: this.code,
13
+ stack: this.stack,
14
+ }
5
15
  }
6
16
  }
7
17
 
8
18
  export class ParseError extends MikkError {
9
- constructor(file: string, cause: string) {
10
- super(`Failed to parse ${file}: ${cause}`, 'PARSE_ERROR')
19
+ constructor(file: string, cause: string | Error) {
20
+ const message = cause instanceof Error
21
+ ? `Failed to parse ${file}: ${cause.message}`
22
+ : `Failed to parse ${file}: ${cause}`
23
+ super(message, 'PARSE_ERROR')
11
24
  }
12
25
  }
13
26
 
@@ -18,8 +31,11 @@ export class ContractNotFoundError extends MikkError {
18
31
  }
19
32
 
20
33
  export class LockNotFoundError extends MikkError {
21
- constructor() {
22
- super(`No mikk.lock.json found. Run 'mikk analyze' first.`, 'LOCK_NOT_FOUND')
34
+ constructor(path?: string) {
35
+ const msg = path
36
+ ? `No mikk.lock.json found at ${path}. Run 'mikk analyze' first.`
37
+ : `No mikk.lock.json found. Run 'mikk analyze' first.`
38
+ super(msg, 'LOCK_NOT_FOUND')
23
39
  }
24
40
  }
25
41
 
@@ -40,3 +56,72 @@ export class SyncStateError extends MikkError {
40
56
  super(`Mikk is in ${status} state. Run 'mikk analyze' to sync.`, 'SYNC_STATE_ERROR')
41
57
  }
42
58
  }
59
+
60
+ export class EmbeddingError extends MikkError {
61
+ constructor(message: string, cause?: Error) {
62
+ const fullMessage = cause
63
+ ? `${message}: ${cause.message}`
64
+ : message
65
+ super(fullMessage, 'EMBEDDING_ERROR')
66
+ }
67
+ }
68
+
69
+ export class SearchError extends MikkError {
70
+ constructor(message: string, cause?: Error) {
71
+ const fullMessage = cause
72
+ ? `${message}: ${cause.message}`
73
+ : message
74
+ super(fullMessage, 'SEARCH_ERROR')
75
+ }
76
+ }
77
+
78
+ export class ValidationError extends MikkError {
79
+ constructor(message: string) {
80
+ super(message, 'VALIDATION_ERROR')
81
+ }
82
+ }
83
+
84
+ export class ConfigurationError extends MikkError {
85
+ constructor(message: string) {
86
+ super(message, 'CONFIGURATION_ERROR')
87
+ }
88
+ }
89
+
90
+ export class TimeoutError extends MikkError {
91
+ constructor(operation: string, timeoutMs: number) {
92
+ super(`Operation '${operation}' timed out after ${timeoutMs}ms`, 'TIMEOUT')
93
+ }
94
+ }
95
+
96
+ export class CacheError extends MikkError {
97
+ constructor(message: string, cause?: Error) {
98
+ const fullMessage = cause
99
+ ? `Cache error: ${message}: ${cause.message}`
100
+ : `Cache error: ${message}`
101
+ super(fullMessage, 'CACHE_ERROR')
102
+ }
103
+ }
104
+
105
+ export function isMikkError(error: unknown): error is MikkError {
106
+ return error instanceof MikkError
107
+ }
108
+
109
+ export function getErrorCode(error: unknown): string {
110
+ if (error instanceof MikkError) {
111
+ return error.code
112
+ }
113
+ if (error instanceof Error) {
114
+ return error.name.toUpperCase().replace(/\s+/g, '_')
115
+ }
116
+ return 'UNKNOWN'
117
+ }
118
+
119
+ export function formatError(error: unknown): string {
120
+ if (isMikkError(error)) {
121
+ return `[${error.code}] ${error.message}`
122
+ }
123
+ if (error instanceof Error) {
124
+ return `${error.name}: ${error.message}`
125
+ }
126
+ return String(error)
127
+ }
package/src/utils/fs.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  import * as fs from 'node:fs/promises'
2
3
  import * as path from 'node:path'
3
4
  import fg from 'fast-glob'
@@ -184,7 +185,22 @@ export function parseMikkIgnore(content: string): string[] {
184
185
  * This is technology-agnostic: it works for Prisma, Drizzle, GraphQL, SQL,
185
186
  * Protobuf, Docker, OpenAPI, and more -- anything with a well-known file pattern.
186
187
  */
187
- export async function discoverContextFiles(projectRoot: string): Promise<ContextFile[]> {
188
+
189
+ export interface DiscoverContextFilesOptions {
190
+ /** Maximum number of context files to return (default 20) */
191
+ maxFiles?: number
192
+ /** Callback for progress updates */
193
+ onProgress?: (current: number, total: number, file: string) => void
194
+ /** Skip reading file content - just get file list with stats */
195
+ metadataOnly?: boolean
196
+ }
197
+
198
+ export async function discoverContextFiles(
199
+ projectRoot: string,
200
+ options: DiscoverContextFilesOptions = {}
201
+ ): Promise<ContextFile[]> {
202
+ const { maxFiles = 20, onProgress, metadataOnly = false } = options
203
+
188
204
  const mikkIgnore = await readMikkIgnore(projectRoot)
189
205
  const files = await fg(CONTEXT_FILE_PATTERNS, {
190
206
  cwd: projectRoot,
@@ -194,29 +210,49 @@ export async function discoverContextFiles(projectRoot: string): Promise<Context
194
210
  })
195
211
 
196
212
  const normalised = files.map(f => f.replace(/\\/g, '/'))
197
-
198
- // Deduplicate -- some patterns overlap (e.g. models/*.ts also matched by source discovery)
199
213
  const unique = [...new Set(normalised)]
200
214
 
201
215
  const results: ContextFile[] = []
202
-
203
- for (const relPath of unique) {
204
- const absPath = path.join(projectRoot, relPath)
205
- try {
206
- const stat = await fs.stat(absPath)
207
- if (stat.size > MAX_CONTEXT_FILE_SIZE) continue // skip huge files
208
- if (stat.size === 0) continue
209
-
210
- const content = await fs.readFile(absPath, 'utf-8')
211
- const type = inferContextFileType(relPath)
212
-
213
- results.push({ path: relPath, content, type, size: stat.size })
214
- } catch {
215
- // File unreadable -- skip
216
+ const batchSize = 10
217
+
218
+ for (let i = 0; i < unique.length; i += batchSize) {
219
+ const batch = unique.slice(i, i + batchSize)
220
+
221
+ const batchResults = await Promise.all(
222
+ batch.map(async (relPath) => {
223
+ const absPath = path.join(projectRoot, relPath)
224
+ try {
225
+ const stat = await fs.stat(absPath)
226
+ if (stat.size > MAX_CONTEXT_FILE_SIZE) return null
227
+ if (stat.size === 0) return null
228
+
229
+ const type = inferContextFileType(relPath)
230
+
231
+ if (onProgress) {
232
+ onProgress(results.length + 1, Math.min(unique.length, maxFiles), relPath)
233
+ }
234
+
235
+ if (metadataOnly) {
236
+ return { path: relPath, content: '', type, size: stat.size }
237
+ }
238
+
239
+ const content = await fs.readFile(absPath, 'utf-8')
240
+ return { path: relPath, content, type, size: stat.size }
241
+ } catch {
242
+ return null
243
+ }
244
+ })
245
+ )
246
+
247
+ for (const result of batchResults) {
248
+ if (result && results.length < maxFiles) {
249
+ results.push(result)
250
+ }
216
251
  }
252
+
253
+ if (results.length >= maxFiles) break
217
254
  }
218
255
 
219
- // Sort: schemas/models first, then types, routes, config
220
256
  const priority: Record<ContextFileType, number> = {
221
257
  schema: 0,
222
258
  model: 1,
@@ -229,9 +265,6 @@ export async function discoverContextFiles(projectRoot: string): Promise<Context
229
265
  }
230
266
  results.sort((a, b) => priority[a.type] - priority[b.type])
231
267
 
232
- // If we have a schema file (e.g. prisma/schema.prisma), the migrations
233
- // are redundant -- they represent historical deltas, not the current state.
234
- // Including them wastes AI tokens and can be actively misleading.
235
268
  const hasSchema = results.some(f => f.type === 'schema')
236
269
  if (hasSchema) {
237
270
  return results.filter(f => f.type !== 'migration')
@@ -287,19 +320,52 @@ export async function detectProjectLanguage(projectRoot: string): Promise<Projec
287
320
  const matches = await fg(pattern, { cwd: projectRoot, onlyFiles: true, deep: 1 })
288
321
  return matches.length > 0
289
322
  }
323
+
324
+ const hasTsConfig = await exists('tsconfig.json') || await hasGlob('tsconfig.*.json')
325
+ const hasPackageJson = await exists('package.json')
326
+ const hasRust = await exists('Cargo.toml')
327
+ const hasGo = await exists('go.mod')
328
+ const hasPython = await exists('pyproject.toml') || await exists('setup.py') || await exists('requirements.txt')
329
+ const hasRuby = await exists('Gemfile')
330
+ const hasJava = await exists('pom.xml') || await exists('build.gradle') || await exists('build.gradle.kts')
331
+ const hasSwift = await exists('Package.swift')
332
+ const hasPhp = await exists('composer.json')
333
+ const hasCSharp = await hasGlob('*.csproj') || await hasGlob('*.sln')
334
+ const hasCpp = await hasGlob('CMakeLists.txt') || await hasGlob('**/*.cmake')
335
+ const hasC = await hasGlob('*.c') || await hasGlob('*.h')
336
+
337
+ // Count non-JS family manifests (TypeScript and JavaScript share package.json, so count them together)
338
+ let languageFamilyCount = 0
339
+ if (hasTsConfig || hasPackageJson) languageFamilyCount++ // JS family (TS or JS)
340
+ if (hasRust) languageFamilyCount++
341
+ if (hasGo) languageFamilyCount++
342
+ if (hasPython) languageFamilyCount++
343
+ if (hasRuby) languageFamilyCount++
344
+ if (hasJava) languageFamilyCount++
345
+ if (hasSwift) languageFamilyCount++
346
+ if (hasPhp) languageFamilyCount++
347
+ if (hasCSharp) languageFamilyCount++
348
+ if (hasCpp) languageFamilyCount++
349
+ if (hasC) languageFamilyCount++
350
+
351
+ // If multiple language families detected, it's polyglot
352
+ if (languageFamilyCount > 1) {
353
+ return 'polyglot'
354
+ }
355
+
290
356
  // Check in priority order -- most specific first
291
- if (await exists('tsconfig.json') || await hasGlob('tsconfig.*.json')) return 'typescript'
292
- if (await exists('Cargo.toml')) return 'rust'
293
- if (await exists('go.mod')) return 'go'
294
- if (await exists('pyproject.toml') || await exists('setup.py') || await exists('requirements.txt')) return 'python'
295
- if (await exists('Gemfile')) return 'ruby'
296
- if (await exists('pom.xml') || await exists('build.gradle') || await exists('build.gradle.kts')) return 'java'
297
- if (await exists('Package.swift')) return 'swift'
298
- if (await exists('composer.json')) return 'php'
299
- if (await hasGlob('*.csproj') || await hasGlob('*.sln')) return 'csharp'
300
- if (await hasGlob('CMakeLists.txt') || await hasGlob('**/*.cmake') || await hasGlob('*.cpp')) return 'cpp'
301
- if (await hasGlob('*.c') || await hasGlob('*.h')) return 'c'
302
- if (await exists('package.json')) return 'javascript'
357
+ if (hasTsConfig) return 'typescript'
358
+ if (hasRust) return 'rust'
359
+ if (hasGo) return 'go'
360
+ if (hasPython) return 'python'
361
+ if (hasRuby) return 'ruby'
362
+ if (hasJava) return 'java'
363
+ if (hasSwift) return 'swift'
364
+ if (hasPhp) return 'php'
365
+ if (hasCSharp) return 'csharp'
366
+ if (hasCpp) return 'cpp'
367
+ if (hasC) return 'c'
368
+ if (hasPackageJson) return 'javascript'
303
369
  return 'unknown'
304
370
  }
305
371
 
@@ -320,12 +386,12 @@ export function getDiscoveryPatterns(language: ProjectLanguage): { patterns: str
320
386
  switch (language) {
321
387
  case 'typescript':
322
388
  return {
323
- patterns: toPatterns(language),
389
+ patterns: [...toPatterns(language), '**/*.js', '**/*.jsx'],
324
390
  ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/.nuxt/**', '**/.svelte-kit/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}', '**/venv/**', '**/.venv/**'],
325
391
  }
326
392
  case 'javascript':
327
393
  return {
328
- patterns: toPatterns(language),
394
+ patterns: [...toPatterns(language), '**/*.ts', '**/*.tsx'],
329
395
  ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}', '**/venv/**', '**/.venv/**'],
330
396
  }
331
397
  case 'python':
@@ -345,7 +411,7 @@ export function getDiscoveryPatterns(language: ProjectLanguage): { patterns: str
345
411
  }
346
412
  case 'java':
347
413
  return {
348
- patterns: toPatterns(language),
414
+ patterns: [...toPatterns(language), '**/*.kt', '**/*.kts'],
349
415
  ignore: [...commonIgnore, '**/target/**', '**/.gradle/**', '**/Test*.java', '**/*Test.java'],
350
416
  }
351
417
  case 'swift':
@@ -448,29 +514,17 @@ export async function fileExists(filePath: string): Promise<boolean> {
448
514
 
449
515
  /**
450
516
  * Set up the .mikk directory structure in a project root.
517
+ * Only creates directories that are actually used.
451
518
  */
452
519
  export async function setupMikkDirectory(projectRoot: string): Promise<void> {
453
520
  const dirs = [
454
521
  '.mikk',
455
- '.mikk/fragments',
456
- '.mikk/diagrams',
457
- '.mikk/diagrams/modules',
458
- '.mikk/diagrams/capsules',
459
- '.mikk/diagrams/flows',
460
- '.mikk/diagrams/impact',
461
- '.mikk/diagrams/exposure',
462
- '.mikk/intent',
463
522
  '.mikk/cache',
523
+ '.mikk/transactions',
464
524
  ]
465
525
  for (const dir of dirs) {
466
526
  await fs.mkdir(path.join(projectRoot, dir), { recursive: true })
467
527
  }
468
-
469
- // Create .gitkeep in impact dir
470
- const impactKeep = path.join(projectRoot, '.mikk/diagrams/impact/.gitkeep')
471
- if (!await fileExists(impactKeep)) {
472
- await fs.writeFile(impactKeep, '', 'utf-8')
473
- }
474
528
  }
475
529
 
476
530
  // --- .mikkignore auto-generation --------------------------------------------
package/src/utils/json.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  import * as fs from 'node:fs/promises'
2
3
 
3
4
  /**
@@ -14,15 +14,38 @@ export type RegistryLanguage =
14
14
  | 'csharp'
15
15
  | 'c'
16
16
  | 'cpp'
17
+ | 'zig'
18
+ | 'elixir'
19
+ | 'haskell'
20
+ | 'scala'
21
+ | 'dart'
22
+ | 'lua'
23
+ | 'julia'
24
+ | 'clojure'
25
+ | 'fsharp'
26
+ | 'ocaml'
27
+ | 'perl'
28
+ | 'r'
29
+ | 'sql'
30
+ | 'terraform'
31
+ | 'shell'
32
+ | 'vue'
33
+ | 'svelte'
34
+ | 'jsx'
35
+ | 'tsx'
17
36
  | 'polyglot'
18
37
  | 'unknown'
19
38
 
20
- const OXC_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'] as const
39
+ const OXC_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte'] as const
21
40
  const GO_EXTENSIONS = ['.go'] as const
22
41
  const TREE_SITTER_EXTENSIONS = [
23
42
  '.py', '.java', '.kt', '.kts', '.swift',
24
43
  '.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh',
25
44
  '.cs', '.rs', '.php', '.rb',
45
+ '.zig', '.ex', '.exs', '.hs', '.scala', '.sc',
46
+ '.dart', '.lua', '.jl', '.clj', '.cljs', '.fs', '.fsx',
47
+ '.ml', '.mli', '.pl', '.pm', '.r', '.R', '.sql',
48
+ '.tf', '.sh', '.bash', '.zsh',
26
49
  ] as const
27
50
 
28
51
  const PARSER_EXTENSIONS: Record<Exclude<ParserKind, 'unknown'>, readonly string[]> = {
@@ -32,22 +55,41 @@ const PARSER_EXTENSIONS: Record<Exclude<ParserKind, 'unknown'>, readonly string[
32
55
  }
33
56
 
34
57
  const LANGUAGE_EXTENSIONS: Record<RegistryLanguage, readonly string[]> = {
35
- typescript: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
36
- javascript: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'],
37
- python: ['.py'],
58
+ typescript: ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'],
59
+ javascript: ['.js', '.jsx', '.mjs', '.cjs'],
60
+ vue: ['.vue'],
61
+ svelte: ['.svelte'],
62
+ jsx: ['.jsx'],
63
+ tsx: ['.tsx'],
64
+ python: ['.py', '.pyw'],
38
65
  go: ['.go'],
39
66
  rust: ['.rs'],
40
67
  kotlin: ['.kt', '.kts'],
41
- java: ['.java', '.kt', '.kts'],
68
+ java: ['.java'],
42
69
  swift: ['.swift'],
43
70
  ruby: ['.rb'],
44
71
  php: ['.php'],
45
72
  csharp: ['.cs'],
46
73
  c: ['.c', '.h'],
47
74
  cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh', '.h'],
75
+ zig: ['.zig'],
76
+ elixir: ['.ex', '.exs'],
77
+ haskell: ['.hs'],
78
+ scala: ['.scala', '.sc'],
79
+ dart: ['.dart'],
80
+ lua: ['.lua'],
81
+ julia: ['.jl'],
82
+ clojure: ['.clj', '.cljs', '.cljc'],
83
+ fsharp: ['.fs', '.fsx', '.fsi'],
84
+ ocaml: ['.ml', '.mli'],
85
+ perl: ['.pl', '.pm'],
86
+ r: ['.r', '.R'],
87
+ sql: ['.sql'],
88
+ terraform: ['.tf'],
89
+ shell: ['.sh', '.bash', '.zsh'],
48
90
  polyglot: [
49
91
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
50
- '.py',
92
+ '.py', '.vue', '.svelte',
51
93
  '.go',
52
94
  '.rs',
53
95
  '.java', '.kt', '.kts',
@@ -56,6 +98,10 @@ const LANGUAGE_EXTENSIONS: Record<RegistryLanguage, readonly string[]> = {
56
98
  '.php',
57
99
  '.cs',
58
100
  '.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh',
101
+ '.zig', '.ex', '.exs', '.hs', '.scala', '.sc',
102
+ '.dart', '.lua', '.jl', '.clj', '.cljs', '.cljc',
103
+ '.fs', '.fsx', '.fsi', '.ml', '.mli', '.pl', '.pm',
104
+ '.r', '.R', '.sql', '.tf', '.sh', '.bash', '.zsh',
59
105
  ],
60
106
  unknown: ['.ts', '.tsx', '.js', '.jsx'],
61
107
  }
@@ -82,6 +128,38 @@ export function languageForExtension(ext: string): RegistryLanguage {
82
128
  return EXT_TO_LANGUAGE.get(ext.toLowerCase()) ?? 'unknown'
83
129
  }
84
130
 
131
+ const VALID_PARSED_FILE_LANGUAGES = new Set([
132
+ // Mainstream Languages (22)
133
+ 'javascript', 'typescript', 'python', 'java', 'csharp', 'cpp', 'c',
134
+ 'php', 'ruby', 'swift', 'go', 'kotlin', 'rust', 'dart', 'scala',
135
+ 'haskell', 'elixir', 'clojure', 'fsharp', 'ocaml', 'perl', 'r',
136
+ // Systems Languages
137
+ 'zig',
138
+ // Scripting Languages
139
+ 'lua', 'julia',
140
+ // Special Purpose
141
+ 'sql', 'terraform', 'shell',
142
+ // Web Frameworks
143
+ 'vue', 'svelte',
144
+ // Fallback
145
+ 'unknown'
146
+ ])
147
+
148
+ export function toParsedFileLanguage(lang: RegistryLanguage): ParsedFileLanguage {
149
+ return VALID_PARSED_FILE_LANGUAGES.has(lang)
150
+ ? lang as ParsedFileLanguage
151
+ : 'unknown'
152
+ }
153
+
154
+ export type ParsedFileLanguage =
155
+ | 'javascript' | 'typescript' | 'python' | 'java' | 'csharp' | 'cpp' | 'c'
156
+ | 'php' | 'ruby' | 'swift' | 'go' | 'kotlin' | 'rust' | 'dart' | 'scala'
157
+ | 'haskell' | 'elixir' | 'clojure' | 'fsharp' | 'ocaml' | 'perl' | 'r'
158
+ | 'zig' | 'lua' | 'julia'
159
+ | 'sql' | 'terraform' | 'shell'
160
+ | 'vue' | 'svelte'
161
+ | 'unknown'
162
+
85
163
  export function getParserExtensions(kind: Exclude<ParserKind, 'unknown'>): readonly string[] {
86
164
  return PARSER_EXTENSIONS[kind]
87
165
  }
@@ -0,0 +1,26 @@
1
+ export function normalizeSlashes(filePath: string): string {
2
+ return filePath.replace(/\\/g, '/')
3
+ }
4
+
5
+ export function normalizePath(filePath: string, lowercase: boolean = true): string {
6
+ const normalized = normalizeSlashes(filePath)
7
+ return lowercase ? normalized.toLowerCase() : normalized
8
+ }
9
+
10
+ export function normalizePathQuiet(filePath: string): string {
11
+ return normalizeSlashes(filePath).toLowerCase()
12
+ }
13
+
14
+ export function getPathKey(filePath: string): string {
15
+ return normalizePath(filePath, true)
16
+ }
17
+
18
+ export function pathsEqual(a: string, b: string): boolean {
19
+ return normalizePathQuiet(a) === normalizePathQuiet(b)
20
+ }
21
+
22
+ export function isSubPath(child: string, parent: string): boolean {
23
+ const childNorm = normalizePathQuiet(child)
24
+ const parentNorm = normalizePathQuiet(parent)
25
+ return childNorm.startsWith(parentNorm + '/') || childNorm === parentNorm
26
+ }
@@ -1,9 +1,10 @@
1
1
  import { describe, it, expect } from 'bun:test'
2
2
  import { DeadCodeDetector } from '../src/graph/dead-code-detector'
3
- import { buildTestGraph, mockFunction } from './helpers'
4
- import { GraphBuilder } from '../src/graph/graph-builder'
3
+ import { buildTestGraph } from './helpers'
5
4
  import type { MikkLock } from '../src/contract/schema'
6
5
 
6
+ const _GraphBuilder = { addNode: () => {}, addEdge: () => {}, build: () => new Map() }
7
+
7
8
  /** Helper to generate a dummy lock file from graph nodes for the detector */
8
9
  function generateDummyLock(graphNodes: Map<string, any>): MikkLock {
9
10
  const lock: MikkLock = {