@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,83 @@
1
+ import { ResolverFactory } from 'oxc-resolver';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import type { ParsedFile } from './types.js';
5
+
6
+ /**
7
+ * OxcResolver — Rust-backed compiler-grade module resolution.
8
+ *
9
+ * Resolution strategy:
10
+ * 1. If the resolved path is inside projectRoot → return project-relative posix path
11
+ * 2. If the resolved path is outside projectRoot (node_modules, monorepo peer) → return ''
12
+ * (external deps produce no graph edges; they're not in our file set)
13
+ * 3. On any error → return '' (unresolved, no false edges)
14
+ *
15
+ * All returned paths use forward slashes and are ABSOLUTE, matching what
16
+ * parseFiles passes to parse(). graph-builder only creates import edges when
17
+ * the target path exists as a file node — so no path format inconsistency can
18
+ * create false positive edges.
19
+ */
20
+ export class OxcResolver {
21
+ private resolver: any;
22
+ private readonly normalizedRoot: string;
23
+
24
+ constructor(private readonly projectRoot: string) {
25
+ this.normalizedRoot = path.resolve(projectRoot).replace(/\\/g, '/');
26
+
27
+ const tsconfigPath = path.resolve(projectRoot, 'tsconfig.json');
28
+ const hasTsConfig = fs.existsSync(tsconfigPath);
29
+
30
+ this.resolver = new ResolverFactory({
31
+ extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.mjs', '.cjs', '.mts', '.cts'],
32
+ mainFields: ['module', 'main', 'jsnext:main'],
33
+ mainFiles: ['index', 'main', 'app'],
34
+ conditionNames: ['import', 'require', 'node', 'default', 'types', 'browser'],
35
+ symlinks: true,
36
+ modules: ['node_modules'],
37
+ tsconfig: hasTsConfig ? {
38
+ configFile: tsconfigPath,
39
+ references: 'auto',
40
+ } : undefined,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Resolve a single import source string relative to fromFile.
46
+ * fromFile MUST be an absolute path (as produced by parseFiles).
47
+ * Returns an absolute posix path, or '' if unresolvable/external.
48
+ */
49
+ public resolve(source: string, fromFile: string): string {
50
+ try {
51
+ const absFrom = path.isAbsolute(fromFile)
52
+ ? fromFile
53
+ : path.resolve(this.projectRoot, fromFile);
54
+ const dir = path.dirname(absFrom);
55
+
56
+ const result = this.resolver.sync(dir, source);
57
+ if (!result?.path) return '';
58
+
59
+ const resolved = result.path.replace(/\\/g, '/');
60
+
61
+ // Only include files within our project root in the graph.
62
+ // node_modules, hoisted workspace deps, etc. are external.
63
+ if (!resolved.startsWith(this.normalizedRoot + '/') && resolved !== this.normalizedRoot) {
64
+ return '';
65
+ }
66
+
67
+ return resolved;
68
+ } catch {
69
+ return '';
70
+ }
71
+ }
72
+
73
+ /** Resolve all imports for a batch of files in one pass */
74
+ public resolveBatch(files: ParsedFile[]): ParsedFile[] {
75
+ return files.map(file => ({
76
+ ...file,
77
+ imports: file.imports.map(imp => ({
78
+ ...imp,
79
+ resolvedPath: this.resolve(imp.source, file.path),
80
+ })),
81
+ }));
82
+ }
83
+ }
@@ -109,8 +109,8 @@ function findFirstChild(node: any, predicate: (n: any) => boolean): any {
109
109
  function assignCallsToFunctions(
110
110
  functions: ParsedFunction[],
111
111
  callEntries: Array<{ name: string; line: number }>
112
- ): string[] {
113
- const unassigned: string[] = []
112
+ ): Array<{ name: string; line: number }> {
113
+ const unassigned: Array<{ name: string; line: number }> = []
114
114
  for (const { name, line } of callEntries) {
115
115
  // Find the innermost (smallest range) function that contains this line
116
116
  let best: ParsedFunction | null = null
@@ -125,11 +125,11 @@ function assignCallsToFunctions(
125
125
  }
126
126
  }
127
127
  if (best) {
128
- if (!best.calls.includes(name)) {
129
- best.calls.push(name)
128
+ if (!best.calls.some(c => c.name === name && c.line === line)) {
129
+ best.calls.push({ name, line, type: 'function' })
130
130
  }
131
131
  } else {
132
- unassigned.push(name)
132
+ unassigned.push({ name, line })
133
133
  }
134
134
  }
135
135
  return unassigned
@@ -142,6 +142,7 @@ function assignCallsToFunctions(
142
142
  export class TreeSitterParser extends BaseParser {
143
143
  private parser: any = null
144
144
  private languages = new Map<string, any>()
145
+ private nameCounter = new Map<string, number>()
145
146
 
146
147
  getSupportedExtensions(): string[] {
147
148
  return ['.py', '.java', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.go', '.rs', '.php', '.rb']
@@ -155,6 +156,7 @@ export class TreeSitterParser extends BaseParser {
155
156
  }
156
157
 
157
158
  async parse(filePath: string, content: string): Promise<ParsedFile> {
159
+ this.nameCounter.clear()
158
160
  await this.init()
159
161
  const ext = path.extname(filePath).toLowerCase()
160
162
  const config = await this.getLanguageConfig(ext)
@@ -185,10 +187,13 @@ export class TreeSitterParser extends BaseParser {
185
187
  // --- Calls: record name and line position ---
186
188
  if (captures['call.name']) {
187
189
  const callNode = captures['call.name']
188
- callEntries.push({
189
- name: callNode.text,
190
- line: (callNode.startPosition?.row ?? 0) + 1,
191
- })
190
+ const name = callNode.text
191
+ if (name) {
192
+ callEntries.push({
193
+ name,
194
+ line: (callNode.startPosition?.row ?? 0) + 1,
195
+ })
196
+ }
192
197
  continue
193
198
  }
194
199
 
@@ -215,11 +220,12 @@ export class TreeSitterParser extends BaseParser {
215
220
  const startLine = defNode.startPosition.row + 1
216
221
  const endLine = defNode.endPosition.row + 1
217
222
  const nodeText = defNode.text ?? ''
223
+ const count = (this.nameCounter.get(fnName) ?? 0) + 1
224
+ this.nameCounter.set(fnName, count)
218
225
 
219
- // Unique ID: include start line to handle overloads and same-name scoped functions
220
- let fnId = `fn:${filePath}:${fnName}:${startLine}`
226
+ // Unique ID: use stable format with counter for collisions
227
+ let fnId = count === 1 ? `fn:${filePath}:${fnName}` : `fn:${filePath}:${fnName}#${count}`
221
228
  if (seenFnIds.has(fnId)) {
222
- // Extremely rare duplicate — skip rather than corrupt
223
229
  continue
224
230
  }
225
231
  seenFnIds.add(fnId)
@@ -269,7 +275,7 @@ export class TreeSitterParser extends BaseParser {
269
275
  const startLine = defNode.startPosition.row + 1
270
276
  const endLine = defNode.endPosition.row + 1
271
277
  const nodeText = defNode.text ?? ''
272
- const clsId = `cls:${filePath}:${clsName}:${startLine}`
278
+ const clsId = `class:${filePath}:${clsName}` // consistent with ts-extractor
273
279
 
274
280
  if (!classesMap.has(clsId)) {
275
281
  classesMap.set(clsId, {
@@ -279,7 +285,9 @@ export class TreeSitterParser extends BaseParser {
279
285
  startLine,
280
286
  endLine,
281
287
  methods: [],
288
+ properties: [],
282
289
  isExported: isExportedByLanguage(ext, clsName, nodeText),
290
+ hash: hashContent(nodeText),
283
291
  })
284
292
  }
285
293
  }
@@ -300,9 +308,9 @@ export class TreeSitterParser extends BaseParser {
300
308
  endLine: lineCount || 1,
301
309
  params: [],
302
310
  returnType: 'void',
303
- isExported: true,
311
+ isExported: false, // Don't export the synthetic module function
304
312
  isAsync: false,
305
- calls: Array.from(new Set(unassignedCalls)),
313
+ calls: unassignedCalls.map(c => ({ name: c.name, line: c.line, type: 'function' })),
306
314
  hash: '',
307
315
  purpose: 'Module-level initialization code',
308
316
  edgeCasesHandled: [],
@@ -330,6 +338,8 @@ export class TreeSitterParser extends BaseParser {
330
338
  file: filePath,
331
339
  })),
332
340
  routes: [],
341
+ variables: [],
342
+ calls: [],
333
343
  hash: hashContent(content),
334
344
  parsedAt: Date.now(),
335
345
  }
@@ -352,6 +362,8 @@ export class TreeSitterParser extends BaseParser {
352
362
  imports: [],
353
363
  exports: [],
354
364
  routes: [],
365
+ variables: [],
366
+ calls: [],
355
367
  hash: hashContent(content),
356
368
  parsedAt: Date.now(),
357
369
  }
@@ -1,103 +1,130 @@
1
1
  /**
2
2
  * Parser types — data shapes that flow through the entire Mikk system.
3
- * Parser produces them, graph consumes them, contract stores them.
4
3
  */
5
4
 
6
5
  /** A single parameter in a function signature */
7
6
  export interface ParsedParam {
8
- name: string
9
- type: string
10
- optional: boolean
11
- defaultValue?: string
7
+ name: string;
8
+ type: string;
9
+ optional: boolean;
10
+ defaultValue?: string;
12
11
  }
13
12
 
13
+ /** A single call expression found in code (Mikk 2.0) */
14
+ export interface CallExpression {
15
+ name: string;
16
+ line: number;
17
+ type: 'function' | 'method' | 'property';
18
+ arguments?: string[];
19
+ }
20
+
21
+ /** A detailed function declaration */
14
22
  export interface ParsedFunction {
15
- id: string // "fn:auth/verify.ts:verifyToken"
16
- name: string // "verifyToken"
17
- file: string // "src/auth/verify.ts"
18
- moduleId?: string
19
- startLine: number // 14
20
- endLine: number // 28
21
- params: ParsedParam[] // [{name: "token", type: "string"}]
22
- returnType: string // "boolean"
23
- isExported: boolean // true
24
- isAsync: boolean // false
25
- isGenerator?: boolean // true for function* / async function*
26
- typeParameters?: string[] // ["T", "U"] for generic functions
27
- calls: string[] // ["jwtDecode", "findUser"]
28
- hash: string // SHA-256 of the function body
29
- purpose: string // Extracted from JSDoc or comments
30
- edgeCasesHandled: string[] // Found conditions like 'if (!x) return'
31
- errorHandling: { line: number, type: 'try-catch' | 'throw', detail: string }[]
32
- detailedLines: { startLine: number, endLine: number, blockType: string }[]
23
+ id: string; // unique normalized ID (file::name)
24
+ name: string;
25
+ file: string;
26
+ moduleId?: string;
27
+ startLine: number;
28
+ endLine: number;
29
+ params: ParsedParam[];
30
+ returnType: string;
31
+ isExported: boolean;
32
+ isAsync: boolean;
33
+ isGenerator?: boolean;
34
+ typeParameters?: string[];
35
+ calls: CallExpression[]; // Behavioral tracking (Upgraded from string[])
36
+ hash: string;
37
+ purpose: string;
38
+ edgeCasesHandled: string[];
39
+ errorHandling: { line: number; type: 'try-catch' | 'throw'; detail: string }[];
40
+ detailedLines: { startLine: number; endLine: number; blockType: string }[];
33
41
  }
34
42
 
35
43
  /** A single import statement */
36
44
  export interface ParsedImport {
37
- source: string // "../../utils/jwt"
38
- resolvedPath: string // "src/utils/jwt.ts" (absolute within project)
39
- names: string[] // ["jwtDecode", "jwtSign"]
40
- isDefault: boolean // false
41
- isDynamic: boolean // false
45
+ source: string;
46
+ resolvedPath: string;
47
+ names: string[];
48
+ isDefault: boolean;
49
+ isDynamic: boolean;
42
50
  }
43
51
 
44
52
  /** A single exported symbol */
45
53
  export interface ParsedExport {
46
- name: string // "verifyToken"
47
- type: 'function' | 'class' | 'const' | 'type' | 'default' | 'interface'
48
- file: string
54
+ name: string;
55
+ type: 'function' | 'class' | 'const' | 'type' | 'default' | 'interface' | 'variable';
56
+ file: string;
57
+ }
58
+
59
+ /** A single variable or property */
60
+ export interface ParsedVariable {
61
+ id: string;
62
+ name: string;
63
+ type: string;
64
+ file: string;
65
+ line: number;
66
+ isExported: boolean;
67
+ isStatic?: boolean;
68
+ purpose?: string;
49
69
  }
50
70
 
51
71
  /** A parsed class */
52
72
  export interface ParsedClass {
53
- id: string
54
- name: string
55
- file: string
56
- moduleId?: string
57
- startLine: number
58
- endLine: number
59
- methods: ParsedFunction[]
60
- isExported: boolean
61
- decorators?: string[] // ["Injectable", "Controller"]
62
- typeParameters?: string[] // ["T"] for generic classes
63
- purpose?: string
64
- edgeCasesHandled?: string[]
65
- errorHandling?: { line: number, type: 'try-catch' | 'throw', detail: string }[]
73
+ id: string;
74
+ name: string;
75
+ file: string;
76
+ moduleId?: string;
77
+ startLine: number;
78
+ endLine: number;
79
+ methods: ParsedFunction[];
80
+ properties: ParsedVariable[];
81
+ extends?: string;
82
+ implements?: string[];
83
+ isExported: boolean;
84
+ decorators?: string[];
85
+ typeParameters?: string[];
86
+ hash: string;
87
+ purpose?: string;
88
+ edgeCasesHandled?: string[];
89
+ errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[];
66
90
  }
67
91
 
68
- /** A detected HTTP route registration (Express/Koa/Hono style) */
69
- export interface ParsedRoute {
70
- method: string // "GET", "POST", "PUT", "DELETE", "USE", etc.
71
- path: string // "/upload", "/:shortId", "/api"
72
- handler: string // "createZap" or "anonymous"
73
- middlewares: string[] // ["uploadLimiter", "upload.single"]
74
- file: string // "src/Routes/zap.routes.ts"
75
- line: number // 15
92
+ /** A generic declaration (interface, type aliase, etc.) */
93
+ export interface ParsedGeneric {
94
+ id: string;
95
+ name: string;
96
+ type: string; // "interface" | "type"
97
+ file: string;
98
+ startLine: number;
99
+ endLine: number;
100
+ isExported: boolean;
101
+ typeParameters?: string[];
102
+ hash: string;
103
+ purpose?: string;
76
104
  }
77
105
 
78
- /** A generic declaration like interface, type, or constant with metadata */
79
- export interface ParsedGeneric {
80
- id: string
81
- name: string
82
- type: string // "interface" | "type" | "const"
83
- file: string
84
- startLine: number
85
- endLine: number
86
- isExported: boolean
87
- typeParameters?: string[] // ["T", "K"] for generic interfaces/types
88
- purpose?: string
106
+ /** A detected HTTP route registration */
107
+ export interface ParsedRoute {
108
+ method: string;
109
+ path: string;
110
+ handler: string;
111
+ middlewares: string[];
112
+ file: string;
113
+ line: number;
89
114
  }
90
115
 
91
116
  /** Everything extracted from a single file */
92
117
  export interface ParsedFile {
93
- path: string // "src/auth/verify.ts"
94
- language: 'python' | 'go' | 'typescript' | 'javascript' | 'java' | 'c' | 'cpp' | 'csharp' | 'rust' | 'php' | 'ruby' | 'unknown'
95
- functions: ParsedFunction[]
96
- classes: ParsedClass[]
97
- generics: ParsedGeneric[]
98
- imports: ParsedImport[]
99
- exports: ParsedExport[]
100
- routes: ParsedRoute[] // Detected HTTP route registrations
101
- hash: string // SHA-256 of the entire file content
102
- parsedAt: number // Date.now()
118
+ path: string; // normalized absolute path
119
+ language: 'python' | 'go' | 'typescript' | 'javascript' | 'java' | 'c' | 'cpp' | 'csharp' | 'rust' | 'php' | 'ruby' | 'unknown';
120
+ functions: ParsedFunction[];
121
+ classes: ParsedClass[];
122
+ variables: ParsedVariable[];
123
+ generics: ParsedGeneric[];
124
+ imports: ParsedImport[];
125
+ exports: ParsedExport[];
126
+ routes: ParsedRoute[];
127
+ calls: CallExpression[]; // module-level calls
128
+ hash: string;
129
+ parsedAt: number;
103
130
  }