@getmikk/core 1.8.3 → 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.
- package/package.json +3 -1
- package/src/constants.ts +285 -0
- package/src/contract/contract-generator.ts +7 -0
- package/src/contract/index.ts +2 -3
- package/src/contract/lock-compiler.ts +66 -35
- package/src/contract/lock-reader.ts +24 -4
- package/src/contract/schema.ts +21 -0
- package/src/error-handler.ts +430 -0
- package/src/graph/cluster-detector.ts +45 -20
- package/src/graph/confidence-engine.ts +60 -0
- package/src/graph/graph-builder.ts +298 -255
- package/src/graph/impact-analyzer.ts +130 -119
- package/src/graph/index.ts +4 -0
- package/src/graph/memory-manager.ts +345 -0
- package/src/graph/query-engine.ts +79 -0
- package/src/graph/risk-engine.ts +86 -0
- package/src/graph/types.ts +89 -65
- package/src/parser/change-detector.ts +99 -0
- package/src/parser/go/go-extractor.ts +18 -8
- package/src/parser/go/go-parser.ts +2 -0
- package/src/parser/index.ts +88 -38
- package/src/parser/javascript/js-extractor.ts +1 -1
- package/src/parser/javascript/js-parser.ts +2 -0
- package/src/parser/oxc-parser.ts +675 -0
- package/src/parser/oxc-resolver.ts +83 -0
- package/src/parser/tree-sitter/parser.ts +19 -10
- package/src/parser/types.ts +100 -73
- package/src/parser/typescript/ts-extractor.ts +229 -589
- package/src/parser/typescript/ts-parser.ts +16 -171
- package/src/parser/typescript/ts-resolver.ts +11 -1
- package/src/search/bm25.ts +5 -2
- package/src/utils/minimatch.ts +1 -1
- package/tests/contract.test.ts +2 -2
- package/tests/dead-code.test.ts +7 -7
- package/tests/esm-resolver.test.ts +75 -0
- package/tests/graph.test.ts +20 -20
- package/tests/helpers.ts +11 -6
- package/tests/impact-classified.test.ts +37 -41
- package/tests/parser.test.ts +7 -5
- package/tests/ts-parser.test.ts +27 -52
- 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
|
-
|
|
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.
|
|
239
|
-
const targetFile = this.getFileForNode(edge.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
418
|
-
const dirs =
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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)?.
|
|
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
|
+
}
|