@getmikk/core 1.8.2 → 1.9.0

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 (43) hide show
  1. package/package.json +3 -1
  2. package/src/constants.ts +285 -0
  3. package/src/contract/contract-generator.ts +7 -0
  4. package/src/contract/index.ts +2 -3
  5. package/src/contract/lock-compiler.ts +74 -42
  6. package/src/contract/lock-reader.ts +24 -4
  7. package/src/contract/schema.ts +27 -1
  8. package/src/error-handler.ts +430 -0
  9. package/src/graph/cluster-detector.ts +45 -20
  10. package/src/graph/confidence-engine.ts +60 -0
  11. package/src/graph/dead-code-detector.ts +27 -5
  12. package/src/graph/graph-builder.ts +298 -238
  13. package/src/graph/impact-analyzer.ts +131 -114
  14. package/src/graph/index.ts +4 -0
  15. package/src/graph/memory-manager.ts +345 -0
  16. package/src/graph/query-engine.ts +79 -0
  17. package/src/graph/risk-engine.ts +86 -0
  18. package/src/graph/types.ts +89 -64
  19. package/src/parser/boundary-checker.ts +3 -1
  20. package/src/parser/change-detector.ts +99 -0
  21. package/src/parser/go/go-extractor.ts +28 -9
  22. package/src/parser/go/go-parser.ts +2 -0
  23. package/src/parser/index.ts +88 -38
  24. package/src/parser/javascript/js-extractor.ts +1 -1
  25. package/src/parser/javascript/js-parser.ts +2 -0
  26. package/src/parser/oxc-parser.ts +675 -0
  27. package/src/parser/oxc-resolver.ts +83 -0
  28. package/src/parser/tree-sitter/parser.ts +27 -15
  29. package/src/parser/types.ts +100 -73
  30. package/src/parser/typescript/ts-extractor.ts +241 -537
  31. package/src/parser/typescript/ts-parser.ts +16 -171
  32. package/src/parser/typescript/ts-resolver.ts +11 -1
  33. package/src/search/bm25.ts +5 -2
  34. package/src/utils/minimatch.ts +1 -1
  35. package/tests/contract.test.ts +2 -2
  36. package/tests/dead-code.test.ts +7 -7
  37. package/tests/esm-resolver.test.ts +75 -0
  38. package/tests/graph.test.ts +20 -20
  39. package/tests/helpers.ts +11 -6
  40. package/tests/impact-classified.test.ts +37 -41
  41. package/tests/parser.test.ts +7 -5
  42. package/tests/ts-parser.test.ts +27 -52
  43. package/test-output.txt +0 -373
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Standardized Error Handling System
3
+ *
4
+ * Provides consistent error creation, handling, and reporting across the Mikk codebase.
5
+ * Uses centralized error codes and messages from constants.ts.
6
+ */
7
+
8
+ import type { ERROR_CODES } from './constants.js'
9
+ import { ERROR_MESSAGES } from './constants.js'
10
+ type ErrorCodes = keyof typeof ERROR_CODES
11
+
12
+ // ─── Error Types ─────────────────────────────────────────────────────────────
13
+
14
+ export class MikkError extends Error {
15
+ public readonly code: ErrorCodes
16
+ public readonly category: ErrorCategory
17
+ public readonly context: Record<string, unknown>
18
+ public readonly timestamp: Date
19
+ public readonly stack?: string
20
+
21
+ constructor(
22
+ code: ErrorCodes,
23
+ message?: string,
24
+ context: Record<string, unknown> = {},
25
+ cause?: Error
26
+ ) {
27
+ // Get the default message from constants if not provided
28
+ const defaultMessage = getDefaultErrorMessage(code, context)
29
+ const finalMessage = message || defaultMessage
30
+
31
+ super(finalMessage, { cause })
32
+ this.name = 'MikkError'
33
+ this.code = code
34
+ this.category = categorizeError(code)
35
+ this.context = context
36
+ this.timestamp = new Date()
37
+
38
+ // Capture stack trace
39
+ if (Error.captureStackTrace) {
40
+ Error.captureStackTrace(this, MikkError)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Create a human-readable error summary
46
+ */
47
+ toSummary(): string {
48
+ return `[${this.code}] ${this.message}`
49
+ }
50
+
51
+ /**
52
+ * Get detailed error information for logging
53
+ */
54
+ toDetailed(): string {
55
+ const contextStr = Object.keys(this.context).length > 0
56
+ ? `\nContext: ${JSON.stringify(this.context, null, 2)}`
57
+ : ''
58
+ const stackStr = this.stack ? `\nStack: ${this.stack}` : ''
59
+
60
+ return `${this.toSummary()}${contextStr}${stackStr}`
61
+ }
62
+
63
+ /**
64
+ * Convert to JSON for API responses
65
+ */
66
+ toJSON(): {
67
+ code: ErrorCodes
68
+ message: string
69
+ category: ErrorCategory
70
+ context: Record<string, unknown>
71
+ timestamp: string
72
+ } {
73
+ return {
74
+ code: this.code,
75
+ message: this.message,
76
+ category: this.category,
77
+ context: this.context,
78
+ timestamp: this.timestamp.toISOString(),
79
+ }
80
+ }
81
+ }
82
+
83
+ export enum ErrorCategory {
84
+ FILE_SYSTEM = 'FILE_SYSTEM',
85
+ MODULE_LOADING = 'MODULE_LOADING',
86
+ GRAPH = 'GRAPH',
87
+ TOKEN_BUDGET = 'TOKEN_BUDGET',
88
+ VALIDATION = 'VALIDATION',
89
+ NETWORK = 'NETWORK',
90
+ PERFORMANCE = 'PERFORMANCE',
91
+ UNKNOWN = 'UNKNOWN',
92
+ }
93
+
94
+ // ─── Error Creation Helpers ───────────────────────────────────────────────────
95
+
96
+ export class ErrorBuilder {
97
+ private code?: ErrorCodes
98
+ private message?: string
99
+ private context: Record<string, unknown> = {}
100
+ private cause?: Error
101
+
102
+ static create(): ErrorBuilder {
103
+ return new ErrorBuilder()
104
+ }
105
+
106
+ withCode(code: ErrorCodes): ErrorBuilder {
107
+ this.code = code
108
+ return this
109
+ }
110
+
111
+ withMessage(message: string): ErrorBuilder {
112
+ this.message = message
113
+ return this
114
+ }
115
+
116
+ withContext(key: string, value: unknown): ErrorBuilder {
117
+ this.context[key] = value
118
+ return this
119
+ }
120
+
121
+ withContextObject(context: Record<string, unknown>): ErrorBuilder {
122
+ this.context = { ...this.context, ...context }
123
+ return this
124
+ }
125
+
126
+ withCause(cause: Error): ErrorBuilder {
127
+ this.cause = cause
128
+ return this
129
+ }
130
+
131
+ build(): MikkError {
132
+ if (!this.code) {
133
+ throw new Error('Error code is required')
134
+ }
135
+ return new MikkError(this.code, this.message, this.context, this.cause)
136
+ }
137
+ }
138
+
139
+ // ─── Error Handler Utility ───────────────────────────────────────────────────
140
+
141
+ export class ErrorHandler {
142
+ private static instance: ErrorHandler
143
+ private errorListeners: ((error: MikkError) => void)[] = []
144
+
145
+ static getInstance(): ErrorHandler {
146
+ if (!ErrorHandler.instance) {
147
+ ErrorHandler.instance = new ErrorHandler()
148
+ }
149
+ return ErrorHandler.instance
150
+ }
151
+
152
+ /**
153
+ * Add error listener for centralized error handling
154
+ */
155
+ addListener(listener: (error: MikkError) => void): void {
156
+ this.errorListeners.push(listener)
157
+ }
158
+
159
+ /**
160
+ * Remove error listener
161
+ */
162
+ removeListener(listener: (error: MikkError) => void): void {
163
+ const index = this.errorListeners.indexOf(listener)
164
+ if (index > -1) {
165
+ this.errorListeners.splice(index, 1)
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Handle an error - notifies all listeners
171
+ */
172
+ handleError(error: MikkError): void {
173
+ for (const listener of this.errorListeners) {
174
+ try {
175
+ listener(error)
176
+ } catch (listenerError) {
177
+ console.error('Error in error listener:', listenerError)
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Wrap a function with error handling
184
+ */
185
+ wrap<T extends (...args: any[]) => any>(
186
+ fn: T,
187
+ errorCode: ErrorCodes,
188
+ context: Record<string, unknown> = {}
189
+ ): T {
190
+ return ((...args: any[]) => {
191
+ try {
192
+ const result = fn(...args)
193
+
194
+ // Handle async functions
195
+ if (result && typeof result.catch === 'function') {
196
+ return result.catch((error: Error) => {
197
+ const mikkError = ErrorBuilder.create()
198
+ .withCode(errorCode)
199
+ .withCause(error)
200
+ .withContextObject(context)
201
+ .build()
202
+
203
+ this.handleError(mikkError)
204
+ throw mikkError
205
+ })
206
+ }
207
+
208
+ return result
209
+ } catch (error) {
210
+ const mikkError = ErrorBuilder.create()
211
+ .withCode(errorCode)
212
+ .withCause(error as Error)
213
+ .withContextObject(context)
214
+ .build()
215
+
216
+ this.handleError(mikkError)
217
+ throw mikkError
218
+ }
219
+ }) as T
220
+ }
221
+ }
222
+
223
+ // ─── Specialized Error Types ─────────────────────────────────────────────────
224
+
225
+ export class FileSystemError extends MikkError {
226
+ constructor(code: ErrorCodes, filePath: string, cause?: Error) {
227
+ super(code, undefined, { filePath }, cause)
228
+ this.name = 'FileSystemError'
229
+ }
230
+ }
231
+
232
+ export class ModuleLoadError extends MikkError {
233
+ constructor(code: ErrorCodes, moduleName: string, cause?: Error) {
234
+ super(code, undefined, { moduleName }, cause)
235
+ this.name = 'ModuleLoadError'
236
+ }
237
+ }
238
+
239
+ export class GraphError extends MikkError {
240
+ constructor(code: ErrorCodes, nodeId?: string, cause?: Error) {
241
+ super(code, undefined, { nodeId }, cause)
242
+ this.name = 'GraphError'
243
+ }
244
+ }
245
+
246
+ export class TokenBudgetError extends MikkError {
247
+ constructor(code: ErrorCodes, used: number, budget: number) {
248
+ super(code, undefined, { used, budget })
249
+ this.name = 'TokenBudgetError'
250
+ }
251
+ }
252
+
253
+ export class ValidationError extends MikkError {
254
+ constructor(code: ErrorCodes, field: string, value: unknown, message?: string) {
255
+ super(code, message, { field, value })
256
+ this.name = 'ValidationError'
257
+ }
258
+ }
259
+
260
+ // ─── Error Creation Functions ─────────────────────────────────────────────────
261
+
262
+ /**
263
+ * Create a file not found error
264
+ */
265
+ export function createFileNotFoundError(filePath: string): FileSystemError {
266
+ return new FileSystemError('FILE_NOT_FOUND', filePath)
267
+ }
268
+
269
+ /**
270
+ * Create a file too large error
271
+ */
272
+ export function createFileTooLargeError(filePath: string, size: number, limit: number): FileSystemError {
273
+ return new FileSystemError('FILE_TOO_LARGE', filePath)
274
+ }
275
+
276
+ /**
277
+ * Create a permission denied error
278
+ */
279
+ export function createPermissionDeniedError(path: string): FileSystemError {
280
+ return new FileSystemError('PERMISSION_DENIED', path)
281
+ }
282
+
283
+ /**
284
+ * Create a module not found error
285
+ */
286
+ export function createModuleNotFoundError(moduleName: string): ModuleLoadError {
287
+ return new ModuleLoadError('MODULE_NOT_FOUND', moduleName)
288
+ }
289
+
290
+ /**
291
+ * Create a module load failed error
292
+ */
293
+ export function createModuleLoadFailedError(moduleName: string, cause?: Error): ModuleLoadError {
294
+ return new ModuleLoadError('MODULE_LOAD_FAILED', moduleName, cause)
295
+ }
296
+
297
+ /**
298
+ * Create a graph build failed error
299
+ */
300
+ export function createGraphBuildFailedError(reason: string): GraphError {
301
+ return new GraphError('GRAPH_BUILD_FAILED', undefined, new Error(reason))
302
+ }
303
+
304
+ /**
305
+ * Create a node not found error
306
+ */
307
+ export function createNodeNotFoundError(nodeId: string): GraphError {
308
+ return new GraphError('NODE_NOT_FOUND', nodeId)
309
+ }
310
+
311
+ /**
312
+ * Create a token budget exceeded error
313
+ */
314
+ export function createTokenBudgetExceededError(used: number, budget: number): TokenBudgetError {
315
+ return new TokenBudgetError('TOKEN_BUDGET_EXCEEDED', used, budget)
316
+ }
317
+
318
+ /**
319
+ * Create a validation error
320
+ */
321
+ export function createValidationError(field: string, value: unknown, reason?: string): ValidationError {
322
+ return new ValidationError('INVALID_INPUT', field, value, reason)
323
+ }
324
+
325
+ // ─── Error Utilities ─────────────────────────────────────────────────────────
326
+
327
+ /**
328
+ * Check if an error is a MikkError
329
+ */
330
+ export function isMikkError(error: unknown): error is MikkError {
331
+ return error instanceof MikkError
332
+ }
333
+
334
+ /**
335
+ * Extract the original cause from an error chain
336
+ */
337
+ export function getRootCause(error: Error): Error {
338
+ let current = error
339
+ while (current.cause && current.cause instanceof Error) {
340
+ current = current.cause
341
+ }
342
+ return current
343
+ }
344
+
345
+ /**
346
+ * Convert any error to a MikkError
347
+ */
348
+ export function toMikkError(error: unknown, defaultCode: ErrorCodes = 'UNKNOWN_ERROR' as ErrorCodes): MikkError {
349
+ if (isMikkError(error)) {
350
+ return error
351
+ }
352
+
353
+ if (error instanceof Error) {
354
+ return new MikkError(defaultCode, error.message, {}, error)
355
+ }
356
+
357
+ if (typeof error === 'string') {
358
+ return new MikkError(defaultCode, error)
359
+ }
360
+
361
+ return new MikkError(defaultCode, 'Unknown error occurred')
362
+ }
363
+
364
+ /**
365
+ * Categorize an error code
366
+ */
367
+ function categorizeError(code: ErrorCodes): ErrorCategory {
368
+ if (code.includes('FILE') || code.includes('DIRECTORY')) {
369
+ return ErrorCategory.FILE_SYSTEM
370
+ }
371
+ if (code.includes('MODULE')) {
372
+ return ErrorCategory.MODULE_LOADING
373
+ }
374
+ if (code.includes('GRAPH') || code.includes('NODE')) {
375
+ return ErrorCategory.GRAPH
376
+ }
377
+ if (code.includes('TOKEN')) {
378
+ return ErrorCategory.TOKEN_BUDGET
379
+ }
380
+ if (code.includes('INVALID') || code.includes('VALIDATION')) {
381
+ return ErrorCategory.VALIDATION
382
+ }
383
+ if (code.includes('TIMEOUT')) {
384
+ return ErrorCategory.PERFORMANCE
385
+ }
386
+
387
+ return ErrorCategory.UNKNOWN
388
+ }
389
+
390
+ /**
391
+ * Get default error message from constants
392
+ */
393
+ function getDefaultErrorMessage(code: ErrorCodes, context: Record<string, unknown>): string {
394
+ const template = ERROR_MESSAGES[code] || 'Unknown error occurred'
395
+
396
+ let message = template as string
397
+
398
+ // Replace template variables
399
+ for (const [key, value] of Object.entries(context)) {
400
+ message = message.replace(new RegExp(`{${key}}`, 'g'), String(value))
401
+ }
402
+
403
+ return message
404
+ }
405
+
406
+ // ─── Default Error Listener ─────────────────────────────────────────────────
407
+
408
+ /**
409
+ * Default error listener that logs to console
410
+ */
411
+ export function createDefaultErrorListener(): (error: MikkError) => void {
412
+ return (error: MikkError) => {
413
+ const timestamp = error.timestamp.toISOString()
414
+ const category = error.category
415
+
416
+ // Use appropriate console method based on category
417
+ const logMethod = category === ErrorCategory.FILE_SYSTEM || category === ErrorCategory.MODULE_LOADING
418
+ ? console.error
419
+ : console.warn
420
+
421
+ logMethod(`[${timestamp}] [${category}] ${error.toSummary()}`)
422
+
423
+ if (process.env.NODE_ENV === 'development') {
424
+ console.debug(error.toDetailed())
425
+ }
426
+ }
427
+ }
428
+
429
+ // Initialize default error listener
430
+ ErrorHandler.getInstance().addListener(createDefaultErrorListener())
@@ -195,8 +195,6 @@ export class ClusterDetector {
195
195
  for (const [name, dupes] of nameCount) {
196
196
  if (dupes.length <= 1) continue
197
197
  for (const cluster of dupes) {
198
- // Try to find a distinctive directory segment from the cluster ID
199
- // e.g. "packages-diagram-generator" → "Diagram Generator"
200
198
  const segments = cluster.id.split('-')
201
199
  .filter(s => s !== 'packages' && s !== 'apps' && s !== 'src')
202
200
  const suffix = segments
@@ -208,7 +206,11 @@ export class ClusterDetector {
208
206
  }
209
207
  }
210
208
 
211
- return merged.sort((a, b) => b.confidence - a.confidence)
209
+ const sorted = merged.sort((a, b) => b.confidence - a.confidence)
210
+ if (sorted.length <= 1 && files.length > 1) {
211
+ return this.buildDirectoryClusters(files)
212
+ }
213
+ return sorted
212
214
  }
213
215
 
214
216
  // ─── Coupling Matrix ──────────────────────────────────────────
@@ -235,8 +237,8 @@ export class ClusterDetector {
235
237
  for (const edge of this.graph.edges) {
236
238
  if (edge.type !== 'imports' && edge.type !== 'calls') continue
237
239
 
238
- const sourceFile = this.getFileForNode(edge.source)
239
- const targetFile = this.getFileForNode(edge.target)
240
+ const sourceFile = this.getFileForNode(edge.from)
241
+ const targetFile = this.getFileForNode(edge.to)
240
242
 
241
243
  if (!sourceFile || !targetFile || sourceFile === targetFile) continue
242
244
  if (!fileSet.has(sourceFile) || !fileSet.has(targetFile)) continue
@@ -317,7 +319,7 @@ export class ClusterDetector {
317
319
  const outEdges = this.graph.outEdges.get(file) || []
318
320
  for (const edge of outEdges) {
319
321
  if (edge.type === 'imports') {
320
- if (fileSet.has(edge.target)) {
322
+ if (fileSet.has(edge.to)) {
321
323
  internalEdges++
322
324
  } else {
323
325
  externalEdges++
@@ -331,10 +333,10 @@ export class ClusterDetector {
331
333
  const containEdges = this.graph.outEdges.get(file) || []
332
334
  for (const containEdge of containEdges) {
333
335
  if (containEdge.type === 'contains') {
334
- const fnOutEdges = this.graph.outEdges.get(containEdge.target) || []
336
+ const fnOutEdges = this.graph.outEdges.get(containEdge.to) || []
335
337
  for (const callEdge of fnOutEdges) {
336
338
  if (callEdge.type === 'calls') {
337
- const targetNode = this.graph.nodes.get(callEdge.target)
339
+ const targetNode = this.graph.nodes.get(callEdge.to)
338
340
  if (targetNode && fileSet.has(targetNode.file)) {
339
341
  internalEdges++
340
342
  } else if (targetNode) {
@@ -380,7 +382,7 @@ export class ClusterDetector {
380
382
  const containEdges = this.graph.outEdges.get(f) || []
381
383
  return containEdges
382
384
  .filter(e => e.type === 'contains')
383
- .map(e => e.target)
385
+ .map(e => e.to)
384
386
  })
385
387
  }
386
388
 
@@ -413,18 +415,16 @@ export class ClusterDetector {
413
415
  * "features/auth/api/route.ts" → "features-auth-api"
414
416
  */
415
417
  private getDirSegments(filePath: string): string {
416
- const parts = filePath.split('/')
417
- // Remove filename (last part with an extension)
418
- const dirs = parts.filter((p, i) => i < parts.length - 1 || !p.includes('.'))
419
- // Drop 'src' prefix — it carries no semantic meaning
420
- const meaningful = dirs.filter(d => d !== 'src' && d !== '')
421
- if (meaningful.length === 0) {
422
- // Fallback: use the filename without extension
423
- const last = parts[parts.length - 1]
418
+ const parts = filePath.replace(/\\/g, '/').split('/')
419
+ const filtered = parts.filter(part => part && part.toLowerCase() !== 'src')
420
+ const dirs = filtered.slice(0, -1) // drop filename
421
+ if (dirs.length === 0) {
422
+ const last = filtered[filtered.length - 1] || ''
424
423
  return last.replace(/\.[^.]+$/, '') || 'unknown'
425
424
  }
426
- // Take up to 3 segments for a unique but concise ID
427
- return meaningful.slice(0, 3).join('-')
425
+ const sliceStart = Math.max(0, dirs.length - 3)
426
+ const meaningful = dirs.slice(sliceStart)
427
+ return meaningful.map(seg => seg.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()).join('-') || 'unknown'
428
428
  }
429
429
 
430
430
  // ─── Cluster Merging ──────────────────────────────────────────
@@ -470,6 +470,31 @@ export class ClusterDetector {
470
470
  return result
471
471
  }
472
472
 
473
+ private buildDirectoryClusters(fileNodes: string[]): ModuleCluster[] {
474
+ const buckets = new Map<string, string[]>()
475
+ for (const file of fileNodes) {
476
+ const bucketId = this.getDirSegments(this.getNodeFile(file))
477
+ const entry = buckets.get(bucketId) ?? []
478
+ entry.push(file)
479
+ buckets.set(bucketId, entry)
480
+ }
481
+
482
+ const clusters: ModuleCluster[] = []
483
+ for (const [id, bucket] of buckets) {
484
+ const filePaths = bucket.map(f => this.getNodeFile(f))
485
+ const functions = this.getFunctionIdsForFiles(bucket)
486
+ clusters.push({
487
+ id,
488
+ files: filePaths,
489
+ confidence: this.computeClusterConfidence(bucket),
490
+ suggestedName: this.inferSemanticName(filePaths, functions),
491
+ functions,
492
+ })
493
+ }
494
+
495
+ return clusters.sort((a, b) => b.confidence - a.confidence)
496
+ }
497
+
473
498
  /** Get the base directory (first meaningful segment) for a set of files */
474
499
  private getBaseDir(files: string[]): string {
475
500
  if (files.length === 0) return 'unknown'
@@ -505,7 +530,7 @@ export class ClusterDetector {
505
530
  private inferSemanticName(filePaths: string[], functionIds: string[]): string {
506
531
  // Collect words from function names
507
532
  const fnLabels = functionIds
508
- .map(id => this.graph.nodes.get(id)?.label ?? '')
533
+ .map(id => this.graph.nodes.get(id)?.name ?? '')
509
534
  .filter(Boolean)
510
535
 
511
536
  // Collect file basenames without extension
@@ -0,0 +1,60 @@
1
+ import type { DependencyGraph } from './types.js'
2
+
3
+ /**
4
+ * Mikk 2.0: Confidence Engine
5
+ * Computes the reliability of impact paths using a decay-based formula.
6
+ * Base edge confidences (direct call = 1.0, fuzzy match = 0.6) are
7
+ * multiplied along the path to determine full path confidence.
8
+ */
9
+ export class ConfidenceEngine {
10
+ constructor(private graph: DependencyGraph) {}
11
+
12
+ /**
13
+ * Compute confidence decay along a specific path of node IDs.
14
+ * @param pathIds Array of node IDs forming a path (e.g. ['A', 'B', 'C'])
15
+ * @returns Cumulative confidence score from 0.0 to 1.0
16
+ */
17
+ public calculatePathConfidence(pathIds: string[]): number {
18
+ if (pathIds.length < 2) return 1.0;
19
+
20
+ let totalConfidence = 1.0;
21
+
22
+ for (let i = 0; i < pathIds.length - 1; i++) {
23
+ const current = pathIds[i];
24
+ const next = pathIds[i + 1];
25
+
26
+ const outEdges = this.graph.outEdges.get(current) || [];
27
+ // Find the highest confidence edge connecting current -> next
28
+ let maxEdgeConfidence = 0.0;
29
+
30
+ for (const edge of outEdges) {
31
+ if (edge.to === next) {
32
+ if (edge.confidence > maxEdgeConfidence) {
33
+ maxEdgeConfidence = edge.confidence;
34
+ }
35
+ }
36
+ }
37
+
38
+ if (maxEdgeConfidence === 0.0) {
39
+ return 0.0; // Path is broken or no valid edge
40
+ }
41
+
42
+ totalConfidence *= maxEdgeConfidence;
43
+ }
44
+
45
+ return totalConfidence;
46
+ }
47
+
48
+ /**
49
+ * Calculates the overall aggregated confidence for a target node
50
+ * by averaging the confidence of all paths leading to it.
51
+ */
52
+ public calculateNodeAggregatedConfidence(paths: string[][]): number {
53
+ if (paths.length === 0) return 1.0;
54
+
55
+ const pathConfidences = paths.map(path => this.calculatePathConfidence(path));
56
+ const sum = pathConfidences.reduce((a, b) => a + b, 0);
57
+
58
+ return Number((sum / paths.length).toFixed(3));
59
+ }
60
+ }
@@ -195,9 +195,31 @@ export class DeadCodeDetector {
195
195
  }
196
196
 
197
197
  private isCalledByExportedInSameFile(fn: MikkLock['functions'][string]): boolean {
198
- for (const callerId of fn.calledBy) {
199
- const caller = this.lock.functions[callerId]
200
- if (caller && caller.isExported && caller.file === fn.file) return true
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).
201
+ const file = fn.file
202
+ 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)
222
+ }
201
223
  }
202
224
  return false
203
225
  }
@@ -213,9 +235,9 @@ export class DeadCodeDetector {
213
235
  * high — none of the above: safe to remove.
214
236
  */
215
237
  private inferConfidence(fn: MikkLock['functions'][string]): DeadCodeConfidence {
238
+ if (DYNAMIC_USAGE_PATTERNS.some(p => p.test(fn.name))) return 'low'
216
239
  if (fn.calledBy.length > 0) return 'medium'
217
240
  if (this.filesWithUnresolvedImports.has(fn.file)) return 'medium'
218
- if (DYNAMIC_USAGE_PATTERNS.some(p => p.test(fn.name))) return 'low'
219
241
  return 'high'
220
242
  }
221
243
 
@@ -240,7 +262,7 @@ export class DeadCodeDetector {
240
262
  if (!this.lock.files) return result
241
263
 
242
264
  for (const [filePath, fileInfo] of Object.entries(this.lock.files)) {
243
- const imports = (fileInfo as any).imports ?? []
265
+ const imports = fileInfo.imports ?? []
244
266
  for (const imp of imports) {
245
267
  if (!imp.resolvedPath || imp.resolvedPath === '') {
246
268
  result.add(filePath)