@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
@@ -1,19 +1,8 @@
1
1
  import type { DependencyGraph } from './types.js'
2
- import type { MikkLock } from '../contract/schema.js'
2
+ import type { MikkLock, MikkLockClass } from '../contract/schema.js'
3
3
 
4
4
  // ─── Types ──────────────────────────────────────────────────────────
5
5
 
6
- /**
7
- * Confidence level for a dead code finding.
8
- *
9
- * high — zero callers, no dynamic patterns, no unresolved imports in the file.
10
- * Safe to remove.
11
- * medium — zero callers via graph, but the file has unresolved imports
12
- * (some calls may not have been traced). Review before removing.
13
- * low — zero graph callers, but the function name or context suggests it
14
- * may be used dynamically (generic names, lifecycle hooks, etc.).
15
- * Do not remove without manual verification.
16
- */
17
6
  export type DeadCodeConfidence = 'high' | 'medium' | 'low'
18
7
 
19
8
  export interface DeadCodeEntry {
@@ -36,31 +25,51 @@ export interface DeadCodeResult {
36
25
 
37
26
  // ─── Exemption patterns ────────────────────────────────────────────
38
27
 
39
- /** Entry-point function names that are never "dead" even with 0 graph callers */
40
28
  const ENTRY_POINT_PATTERNS = [
41
29
  /^(main|bootstrap|start|init|setup|configure|register|mount)$/i,
42
30
  /^(app|server|index|mod|program)$/i,
43
31
  /Handler$/i,
44
32
  /Middleware$/i,
45
33
  /Controller$/i,
46
- /^use[A-Z]/, // React hooks
47
- /^handle[A-Z]/, // Event handlers
48
- /^on[A-Z]/, // Event listeners
34
+ /^use[A-Z]/,
35
+ /^handle[A-Z]/,
36
+ /^on[A-Z]/,
37
+ /Provider$/i,
38
+ /Provider$/,
39
+ /^Page$/i,
40
+ /^Layout$/i,
41
+ /^get[A-Z]/,
42
+ /^default$/i,
43
+ /Provider$/,
44
+ /^(getStaticProps|getServerSideProps|generateStaticParams)$/,
49
45
  ]
50
46
 
51
- /** Test function patterns */
52
47
  const TEST_PATTERNS = [
53
48
  /^(it|describe|test|beforeAll|afterAll|beforeEach|afterEach)$/,
54
49
  /\.test\./,
55
50
  /\.spec\./,
56
51
  /__test__/,
52
+ /_test_/,
53
+ /_spec_/,
54
+ ]
55
+
56
+ const SCRIPT_PATTERNS = [
57
+ /\/scripts\//,
58
+ /\/benchmarks\//,
59
+ /\/fixtures\//,
60
+ /\.bench\./,
61
+ /\.benchmark\./,
62
+ ]
63
+
64
+ const FRAMEWORK_ENTRY_PATTERNS = [
65
+ /\/app\//,
66
+ /\/pages\//,
67
+ /\/components\//,
68
+ /\.next\//,
69
+ /\.mjs$/,
70
+ /\.cjs$/,
57
71
  ]
58
72
 
59
- /**
60
- * Names that are commonly used via dynamic dispatch, string-keyed maps, or
61
- * framework injection. A function matching these patterns gets LOW confidence
62
- * even if no graph callers exist, because static analysis may have missed it.
63
- */
64
73
  const DYNAMIC_USAGE_PATTERNS = [
65
74
  /^addEventListener$/i,
66
75
  /^removeEventListener$/i,
@@ -71,29 +80,39 @@ const DYNAMIC_USAGE_PATTERNS = [
71
80
  /^componentWillUnmount$/i,
72
81
  ]
73
82
 
83
+ const FRAMEWORK_PATTERNS = [
84
+ /^componentDidCatch$/i,
85
+ /^getDerivedStateFromError$/i,
86
+ /^getDerivedStateFromProps$/i,
87
+ /^render$/i,
88
+ /^shouldComponentUpdate$/i,
89
+ /^componentWillReceiveProps$/i,
90
+ /^componentWillUpdate$/i,
91
+ /^UNSAFE_/,
92
+ /^__\w+__$/,
93
+ /^\$\w+/,
94
+ ]
95
+
96
+ const CONSTRUCTOR_PATTERNS = [
97
+ /^constructor$/i,
98
+ /^__construct$/i,
99
+ /^__init__$/i,
100
+ /^init$/i,
101
+ /^initialize$/i,
102
+ ]
103
+
104
+ const CLASS_METHOD_PATTERNS = [
105
+ /\.constructor$/,
106
+ /^\w+\.\w+$/,
107
+ ]
108
+
74
109
  // ─── Detector ──────────────────────────────────────────────────────
75
110
 
76
- /**
77
- * DeadCodeDetector — walks the dependency graph to find functions with zero
78
- * incoming `calls` edges after applying multi-pass exemptions.
79
- *
80
- * Exemptions (function is NOT reported as dead):
81
- * 1. Exported symbols — may be consumed by external packages
82
- * 2. Entry point name patterns — main, handler, middleware, hooks, etc.
83
- * 3. Route handlers — detected via HTTP route registrations in the lock
84
- * 4. Test functions — describe, it, test, lifecycle hooks
85
- * 5. Constructors — called implicitly by `new`
86
- * 6. Called by an exported function in the same file (transitive liveness)
87
- *
88
- * Each dead entry includes a confidence level:
89
- * high — safe to remove
90
- * medium — file has unresolved imports; verify before removing
91
- * low — dynamic usage patterns detected; manual review required
92
- */
93
111
  export class DeadCodeDetector {
94
112
  private routeHandlers: Set<string>
95
- /** Files that have at least one unresolved import (empty resolvedPath) */
96
113
  private filesWithUnresolvedImports: Set<string>
114
+ private fnIndex: Map<number, string>
115
+ private allClasses: Map<string, MikkLockClass>
97
116
 
98
117
  constructor(
99
118
  private graph: DependencyGraph,
@@ -102,10 +121,37 @@ export class DeadCodeDetector {
102
121
  this.routeHandlers = new Set(
103
122
  (lock.routes ?? []).map(r => r.handler).filter(Boolean),
104
123
  )
105
-
106
- // Pre-compute which files have unresolved imports so confidence can
107
- // be lowered for all functions in those files without scanning per-function.
124
+ const rawFnIndex = (lock as any).fnIndex || []
125
+ this.fnIndex = new Map(rawFnIndex.map((id: string, idx: number) => [idx, id]))
108
126
  this.filesWithUnresolvedImports = this.buildUnresolvedImportFileSet()
127
+ this.allClasses = this.buildClassIndex()
128
+ }
129
+
130
+ private resolveFnData(id: string): { name: string; file: string; isExported?: boolean; moduleId?: string } {
131
+ const fn = this.lock.functions[id]
132
+ if (fn?.name && fn?.file) return fn
133
+
134
+ const fullId = this.resolveId(id)
135
+ const resolvedFn = this.lock.functions[fullId]
136
+ if (resolvedFn?.name && resolvedFn?.file) return resolvedFn
137
+
138
+ if (fullId.startsWith('fn:')) {
139
+ const parts = fullId.slice(3).split(':')
140
+ const file = parts.slice(0, -1).join(':')
141
+ const name = parts[parts.length - 1]
142
+ return { name, file }
143
+ }
144
+
145
+ return { name: '', file: '' }
146
+ }
147
+
148
+ private resolveId(id: string): string {
149
+ if (this.lock.functions[id]) return id
150
+ const num = parseInt(id)
151
+ if (!isNaN(num)) {
152
+ return this.fnIndex.get(num) || id
153
+ }
154
+ return id
109
155
  }
110
156
 
111
157
  detect(): DeadCodeResult {
@@ -113,8 +159,14 @@ export class DeadCodeDetector {
113
159
  let totalFunctions = 0
114
160
  const byModule: DeadCodeResult['byModule'] = {}
115
161
 
116
- for (const [id, fn] of Object.entries(this.lock.functions)) {
117
- totalFunctions++
162
+ const functionIds = Object.keys(this.lock.functions)
163
+ totalFunctions = functionIds.length
164
+
165
+ for (const id of functionIds) {
166
+ const fn = this.lock.functions[id]
167
+ if (!fn) continue
168
+
169
+ const fnData = this.resolveFnData(id)
118
170
  const moduleId = fn.moduleId ?? 'unknown'
119
171
 
120
172
  if (!byModule[moduleId]) {
@@ -122,21 +174,22 @@ export class DeadCodeDetector {
122
174
  }
123
175
  byModule[moduleId].total++
124
176
 
125
- // Check for incoming call edges in the graph
126
- const inEdges = this.graph.inEdges.get(id) || []
127
- const hasCallers = inEdges.some(e => e.type === 'calls')
128
- if (hasCallers) continue
177
+ const name = fnData.name
178
+ const file = fnData.file
179
+ const isExported = fn.isExported ?? false
129
180
 
130
- if (this.isExempt(fn, id)) continue
181
+ if (this.isExempt(fn, id, name, file, isExported)) {
182
+ continue
183
+ }
131
184
 
132
- const confidence = this.inferConfidence(fn)
185
+ const confidence = this.computeConfidence(fn, name, file)
133
186
  const entry: DeadCodeEntry = {
134
187
  id,
135
- name: fn.name,
136
- file: fn.file,
188
+ name,
189
+ file,
137
190
  moduleId,
138
191
  type: 'function',
139
- reason: this.inferReason(fn),
192
+ reason: this.computeReason(fn),
140
193
  confidence,
141
194
  }
142
195
  dead.push(entry)
@@ -144,7 +197,6 @@ export class DeadCodeDetector {
144
197
  byModule[moduleId].items.push(entry)
145
198
  }
146
199
 
147
- // Check classes
148
200
  if (this.lock.classes) {
149
201
  for (const [id, cls] of Object.entries(this.lock.classes)) {
150
202
  const moduleId = cls.moduleId ?? 'unknown'
@@ -152,9 +204,11 @@ export class DeadCodeDetector {
152
204
  byModule[moduleId] = { dead: 0, total: 0, items: [] }
153
205
  }
154
206
 
207
+ if (cls.isExported) continue
208
+
155
209
  const inEdges = this.graph.inEdges.get(id) || []
156
- const hasCallers = inEdges.some(e => e.type === 'calls' || e.type === 'imports')
157
- if (hasCallers || cls.isExported) continue
210
+ const hasImporters = inEdges.some(e => e.type === 'imports')
211
+ if (hasImporters) continue
158
212
 
159
213
  const entry: DeadCodeEntry = {
160
214
  id,
@@ -162,7 +216,7 @@ export class DeadCodeDetector {
162
216
  file: cls.file,
163
217
  moduleId,
164
218
  type: 'class',
165
- reason: 'Class has no callers or importers and is not exported',
219
+ reason: 'Class has no importers and is not exported',
166
220
  confidence: this.filesWithUnresolvedImports.has(cls.file) ? 'medium' : 'high',
167
221
  }
168
222
  dead.push(entry)
@@ -182,81 +236,213 @@ export class DeadCodeDetector {
182
236
  }
183
237
  }
184
238
 
185
- // ─── Private helpers ───────────────────────────────────────────
239
+ private isExempt(
240
+ fn: MikkLock['functions'][string],
241
+ id: string,
242
+ name: string,
243
+ file: string,
244
+ isExported: boolean,
245
+ ): boolean {
246
+ if (isExported) return true
247
+ if (this.hasGraphCallers(id, name, file)) return true
248
+ if (this.hasCalledByInLock(fn)) return true
249
+ if (ENTRY_POINT_PATTERNS.some(p => p.test(name))) return true
250
+ if (this.routeHandlers.has(name)) return true
251
+ if (TEST_PATTERNS.some(p => p.test(name) || p.test(file))) return true
252
+ if (SCRIPT_PATTERNS.some(p => p.test(file))) return true
253
+ if (CONSTRUCTOR_PATTERNS.some(p => p.test(name))) return true
254
+ if (FRAMEWORK_PATTERNS.some(p => p.test(name))) return true
255
+ if (this.isReactComponent(name)) return true
256
+ if (this.isCalledByExportedInSameFile(fn, id)) return true
257
+ if (this.isMethodOfUsedClass(fn, name, file)) return true
258
+ if (this.isFrameworkEntryPoint(file, name)) return true
259
+ if (this.isFrameworkEntry(file)) return true
186
260
 
187
- private isExempt(fn: MikkLock['functions'][string], id: string): boolean {
188
- if (fn.isExported) return true
189
- if (ENTRY_POINT_PATTERNS.some(p => p.test(fn.name))) return true
190
- if (this.routeHandlers.has(fn.name)) return true
191
- if (TEST_PATTERNS.some(p => p.test(fn.name) || p.test(fn.file))) return true
192
- if (fn.name === 'constructor' || fn.name === '__init__') return true
193
- if (this.isCalledByExportedInSameFile(fn)) return true
194
261
  return false
195
262
  }
196
263
 
197
- private isCalledByExportedInSameFile(fn: MikkLock['functions'][string]): boolean {
198
- // Multi-pass transitive liveness: propagate liveness through the full calledBy
199
- // chain until no new live functions are discovered. A single-hop check misses
200
- // patterns like: exportedFn → internalA → internalB (internalB is still live).
264
+ private hasGraphCallers(id: string, name: string, file: string): boolean {
265
+ const candidates = [
266
+ id,
267
+ name,
268
+ `fn:${file}:${name}`,
269
+ file + ':' + name,
270
+ ]
271
+
272
+ for (const candidate of candidates) {
273
+ const inEdges = this.graph.inEdges.get(candidate)
274
+ if (inEdges?.some(e => e.type === 'calls')) return true
275
+ }
276
+
277
+ const fn = this.lock.functions[id]
278
+ if (fn?.calledBy?.length) return true
279
+
280
+ return false
281
+ }
282
+
283
+ private isReactComponent(name: string): boolean {
284
+ if (!name || name.length < 2) return false
285
+ if (!/^[A-Z]/.test(name)) return false
286
+ if (name.includes('.') || name.includes('/') || name.includes('\\')) return false
287
+ if (name.includes(':') || name.includes('#')) return false
288
+ if (/^[A-Z][a-z]/.test(name)) return true
289
+ if (/^[A-Z][A-Z]/.test(name)) return true
290
+ return false
291
+ }
292
+
293
+ private isCalledByExportedInSameFile(fn: MikkLock['functions'][string] | undefined, id: string): boolean {
294
+ if (!fn) return false
201
295
  const file = fn.file
296
+ if (!file) return false
297
+
298
+ return this.isReachableFromExported(fn, id, file)
299
+ }
300
+
301
+ private hasCalledByInLock(fn: MikkLock['functions'][string]): boolean {
302
+ if (!fn.calledBy || fn.calledBy.length === 0) return false
303
+
304
+ for (const callerId of fn.calledBy) {
305
+ const caller = this.lock.functions[callerId]
306
+ if (!caller) continue
307
+ if (caller.isExported) return true
308
+ if (this.hasGraphCallers(callerId, caller.name, caller.file)) return true
309
+ }
310
+
311
+ return false
312
+ }
313
+
314
+ private isReachableFromExported(
315
+ startFn: MikkLock['functions'][string],
316
+ startId: string,
317
+ file: string,
318
+ requireExported: boolean = true
319
+ ): boolean {
202
320
  const visited = new Set<string>()
203
- const queue: string[] = [fn.id]
204
-
205
- while (queue.length > 0) {
206
- const currentId = queue.pop()!
207
- if (visited.has(currentId)) continue
208
- visited.add(currentId)
209
-
210
- const current = this.lock.functions[currentId]
211
- if (!current) continue
212
-
213
- for (const callerId of current.calledBy) {
214
- if (visited.has(callerId)) continue
215
- const caller = this.lock.functions[callerId]
216
- if (!caller) continue
217
- // Only follow the chain within the same file
218
- if (caller.file !== file) continue
219
- // Found a live exported caller in the same file — the original fn is live
220
- if (caller.isExported) return true
221
- queue.push(callerId)
321
+ const queue: string[] = [startId]
322
+ const maxDepth = 50
323
+
324
+ let depth = 0
325
+ while (queue.length > 0 && depth < maxDepth) {
326
+ const levelSize = queue.length
327
+ for (let i = 0; i < levelSize; i++) {
328
+ const currentId = queue.shift()!
329
+ if (visited.has(currentId)) continue
330
+ visited.add(currentId)
331
+
332
+ const current = this.lock.functions[currentId]
333
+ if (!current) continue
334
+
335
+ for (const callerId of current.calledBy || []) {
336
+ if (visited.has(callerId)) continue
337
+ const caller = this.lock.functions[callerId]
338
+ if (!caller) continue
339
+ if (caller.file !== file) {
340
+ queue.push(callerId)
341
+ continue
342
+ }
343
+ if (caller.isExported) return true
344
+ if (!requireExported) return true
345
+ queue.push(callerId)
346
+ }
347
+
348
+ const callerEdges = this.graph.inEdges.get(currentId) || []
349
+ for (const edge of callerEdges) {
350
+ if (edge.type !== 'calls') continue
351
+ if (visited.has(edge.from)) continue
352
+ const caller = this.lock.functions[edge.from]
353
+ if (!caller) continue
354
+ if (caller.file !== file) {
355
+ queue.push(edge.from)
356
+ continue
357
+ }
358
+ if (caller.isExported) return true
359
+ if (!requireExported) return true
360
+ queue.push(edge.from)
361
+ }
222
362
  }
363
+ depth++
364
+ }
365
+ return false
366
+ }
367
+
368
+ private isMethodOfUsedClass(fn: MikkLock['functions'][string], name: string, file: string): boolean {
369
+ const classInfo = this.allClasses.get(file)
370
+ if (!classInfo) return false
371
+
372
+ const inEdges = this.graph.inEdges.get(`class:${file}:${classInfo.name}`) || []
373
+ if (inEdges.length > 0) return true
374
+
375
+ for (const [clsId, cls] of Object.entries(this.lock.classes || {})) {
376
+ if (cls.file !== file) continue
377
+ const clsInEdges = this.graph.inEdges.get(clsId) || []
378
+ if (clsInEdges.length > 0) return true
379
+ }
380
+
381
+ return false
382
+ }
383
+
384
+ private buildClassIndex(): Map<string, MikkLockClass> {
385
+ const index = new Map<string, MikkLockClass>()
386
+ if (!this.lock.classes) return index
387
+
388
+ for (const [id, cls] of Object.entries(this.lock.classes)) {
389
+ index.set(cls.file, cls)
390
+ }
391
+ return index
392
+ }
393
+
394
+ private isFrameworkEntryPoint(file: string, name: string): boolean {
395
+ const frameworkExports = [
396
+ /^Page$/,
397
+ /^Layout$/,
398
+ /^default$/,
399
+ /^(getServerSideProps|getStaticProps|generateMetadata|generateViewport)$/,
400
+ ]
401
+
402
+ return frameworkExports.some(p => p.test(name))
403
+ }
404
+
405
+ private isFrameworkEntry(file: string): boolean {
406
+ return FRAMEWORK_ENTRY_PATTERNS.some(p => p.test(file))
407
+ }
408
+
409
+ private hasCalledByFromLock(fn: MikkLock['functions'][string]): boolean {
410
+ if (!fn.calledBy || fn.calledBy.length === 0) return false
411
+
412
+ for (const callerId of fn.calledBy) {
413
+ const caller = this.lock.functions[callerId]
414
+ if (!caller) continue
415
+
416
+ if (caller.isExported) return true
417
+ if (this.hasCallersFromGraph(callerId)) return true
418
+ }
419
+
420
+ return false
421
+ }
422
+
423
+ private hasCallersFromGraph(id: string): boolean {
424
+ const candidates = [id]
425
+ for (const candidate of candidates) {
426
+ const inEdges = this.graph.inEdges.get(candidate)
427
+ if (inEdges?.some(e => e.type === 'calls')) return true
223
428
  }
224
429
  return false
225
430
  }
226
431
 
227
- /**
228
- * Assign a confidence level to a dead code finding.
229
- *
230
- * Priority (first match wins):
231
- * medium — lock.calledBy has entries that didn't become graph edges:
232
- * something references this function but resolution failed.
233
- * medium — file has unresolved imports: the graph may be incomplete.
234
- * low — function name matches common dynamic-dispatch patterns.
235
- * high — none of the above: safe to remove.
236
- */
237
- private inferConfidence(fn: MikkLock['functions'][string]): DeadCodeConfidence {
238
- if (DYNAMIC_USAGE_PATTERNS.some(p => p.test(fn.name))) return 'low'
239
- if (fn.calledBy.length > 0) return 'medium'
240
- if (this.filesWithUnresolvedImports.has(fn.file)) return 'medium'
432
+ private computeConfidence(fn: MikkLock['functions'][string], name: string, file: string): DeadCodeConfidence {
433
+ if (DYNAMIC_USAGE_PATTERNS.some(p => p.test(name))) return 'low'
434
+ if (fn.calledBy?.length) return 'medium'
435
+ if (this.filesWithUnresolvedImports.has(file)) return 'medium'
241
436
  return 'high'
242
437
  }
243
438
 
244
- private inferReason(fn: MikkLock['functions'][string]): string {
245
- if (fn.calledBy.length === 0) {
246
- return 'No callers found anywhere in the codebase'
439
+ private computeReason(fn: MikkLock['functions'][string]): string {
440
+ if (fn.calledBy?.length) {
441
+ return `${fn.calledBy.length} reference(s) in lock but no active call edges in graph`
247
442
  }
248
- return `${fn.calledBy.length} reference(s) in lock but none resolved to active call edges`
443
+ return 'No callers found in graph'
249
444
  }
250
445
 
251
- /**
252
- * Build the set of file paths that have at least one import whose
253
- * resolvedPath is empty. Used to downgrade confidence for all dead
254
- * findings in those files, since the graph may be incomplete.
255
- *
256
- * We derive this from the lock's file entries. Each file entry stores
257
- * its imports; any import with an empty resolvedPath (or no match in
258
- * the graph nodes) indicates an unresolved dependency.
259
- */
260
446
  private buildUnresolvedImportFileSet(): Set<string> {
261
447
  const result = new Set<string>()
262
448
  if (!this.lock.files) return result
@@ -266,7 +452,7 @@ export class DeadCodeDetector {
266
452
  for (const imp of imports) {
267
453
  if (!imp.resolvedPath || imp.resolvedPath === '') {
268
454
  result.add(filePath)
269
- break // One unresolved import is enough to flag the file
455
+ break
270
456
  }
271
457
  }
272
458
  }