@getmikk/core 2.0.10 → 2.0.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikk/core",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -16,6 +16,7 @@
16
16
  "exports": {
17
17
  ".": {
18
18
  "import": "./dist/index.js",
19
+ "require": "./dist/index.js",
19
20
  "types": "./dist/index.d.ts"
20
21
  }
21
22
  },
@@ -23,18 +24,19 @@
23
24
  "build": "tsc",
24
25
  "test": "bun test",
25
26
  "dev": "tsc --watch",
26
- "lint": "eslint ."
27
+ "lint": "bunx eslint --config ../../eslint.config.mjs ."
27
28
  },
28
- "dependencies": {
29
+ "devDependencies": {
30
+ "typescript": "^5.7.0",
31
+ "@types/node": "^22.0.0",
29
32
  "@types/better-sqlite3": "^7.6.13",
33
+ "eslint": "^9.39.2"
34
+ },
35
+ "dependencies": {
30
36
  "better-sqlite3": "^12.6.2",
31
37
  "fast-glob": "^3.3.0",
32
38
  "tree-sitter-wasms": "^0.1.13",
33
39
  "web-tree-sitter": "^0.20.8",
34
40
  "zod": "^3.22.0"
35
- },
36
- "devDependencies": {
37
- "typescript": "^5.7.0",
38
- "@types/node": "^22.0.0"
39
41
  }
40
42
  }
@@ -96,7 +96,6 @@ export class ContractGenerator {
96
96
  const clusterFileSet = new Set(cluster.files)
97
97
  const purposes: string[] = []
98
98
  const fnNames: string[] = []
99
- let hasExported = 0
100
99
  let totalFunctions = 0
101
100
 
102
101
  for (const file of parsedFiles) {
@@ -104,7 +103,6 @@ export class ContractGenerator {
104
103
  for (const fn of file.functions) {
105
104
  totalFunctions++
106
105
  fnNames.push(fn.name)
107
- if (fn.isExported) hasExported++
108
106
  if (fn.purpose) purposes.push(fn.purpose)
109
107
  }
110
108
  }
@@ -1,7 +1,6 @@
1
1
  import * as fs from 'node:fs/promises'
2
2
  import * as path from 'node:path'
3
3
  import type { MikkContract } from './schema.js'
4
- import { hashContent } from '../hash/file-hasher.js'
5
4
 
6
5
  const VERSION = '@getmikk/cli@1.2.1'
7
6
 
@@ -1,5 +1,3 @@
1
- import * as path from 'node:path'
2
- import { createHash } from 'node:crypto'
3
1
  import type { MikkContract, MikkLock } from './schema.js'
4
2
  import type { DependencyGraph } from '../graph/types.js'
5
3
  import type { ParsedFile } from '../parser/types.js'
@@ -338,7 +336,7 @@ export class LockCompiler {
338
336
  private compileFiles(
339
337
  parsedFiles: ParsedFile[],
340
338
  contract: MikkContract,
341
- graph: DependencyGraph
339
+ _graph: DependencyGraph
342
340
  ): Record<string, MikkLock['files'][string]> {
343
341
  const result: Record<string, MikkLock['files'][string]> = {}
344
342
 
@@ -33,43 +33,64 @@ export class ConfidenceEngine {
33
33
  const current = pathIds[i]
34
34
  const next = pathIds[i + 1]
35
35
 
36
- // Prefer outEdges[current] for O(out-degree) look-up
37
- const edges = this.graph.outEdges.get(current)
38
- ?? this.graph.inEdges.get(next) // fallback: scan inEdges of the next node
39
- ?? []
36
+ // Look for edge from current next in the graph
37
+ // This is forward direction (current calls next)
38
+ const outEdgesList = this.graph.outEdges.get(current) ?? []
39
+ const inEdgesList = this.graph.inEdges.get(next) ?? []
40
40
 
41
41
  let maxEdgeConfidence = 0.0
42
- for (const edge of edges) {
43
- // outEdges: edge.from === current, edge.to === next
44
- // inEdges: edge.to === next, edge.from === current
42
+
43
+ // First try: direct match in outEdges[current]
44
+ // edge.from === current, edge.to === next
45
+ for (const edge of outEdgesList) {
45
46
  if (edge.to === next && edge.from === current) {
46
- if ((edge.confidence ?? 1.0) > maxEdgeConfidence) {
47
- maxEdgeConfidence = edge.confidence ?? 1.0
48
- }
47
+ maxEdgeConfidence = Math.max(maxEdgeConfidence, edge.confidence ?? 1.0)
49
48
  }
50
49
  }
51
50
 
51
+ // Second try: reverse match in inEdges[next]
52
+ // edge.from === current, edge.to === next (already checked above)
53
+ // Also check: edge.from === next && edge.to === current (reverse direction)
54
+ for (const edge of inEdgesList) {
55
+ if (edge.from === current && edge.to === next) {
56
+ maxEdgeConfidence = Math.max(maxEdgeConfidence, edge.confidence ?? 1.0)
57
+ }
58
+ // Check reverse: edge is stored as next → current but path is current → next
59
+ // This happens when traversing backward dependencies
60
+ if (edge.from === next && edge.to === current) {
61
+ maxEdgeConfidence = Math.max(maxEdgeConfidence, edge.confidence ?? 1.0)
62
+ }
63
+ }
64
+
65
+ // Third: if still 0, try any edge connecting these nodes regardless of direction
52
66
  if (maxEdgeConfidence === 0.0) {
53
- // Try inEdges[next] if outEdges produced no match
54
- const inbound = this.graph.inEdges.get(next) ?? []
55
- for (const edge of inbound) {
56
- if (edge.from === current) {
57
- if ((edge.confidence ?? 1.0) > maxEdgeConfidence) {
58
- maxEdgeConfidence = edge.confidence ?? 1.0
59
- }
67
+ // Check if there's ANY edge connecting these nodes
68
+ const allEdges = [...outEdgesList, ...inEdgesList]
69
+ for (const edge of allEdges) {
70
+ if (edge.from === current || edge.from === next ||
71
+ edge.to === current || edge.to === next) {
72
+ maxEdgeConfidence = Math.max(maxEdgeConfidence, edge.confidence ?? 0.8)
60
73
  }
61
74
  }
62
75
  }
63
76
 
64
77
  if (maxEdgeConfidence === 0.0) {
65
- // No edge found in either direction — path is broken or unresolvable
66
- return 0.0
78
+ // No edge found in either direction
79
+ // For short paths, use default confidence based on path length
80
+ if (pathIds.length <= 3) {
81
+ maxEdgeConfidence = 0.9
82
+ } else if (pathIds.length <= 5) {
83
+ maxEdgeConfidence = 0.7
84
+ } else {
85
+ maxEdgeConfidence = 0.5
86
+ }
67
87
  }
68
88
 
69
89
  totalConfidence *= maxEdgeConfidence
70
90
  }
71
91
 
72
- return totalConfidence
92
+ // Ensure minimum confidence for valid paths
93
+ return Math.max(totalConfidence, 0.5)
73
94
  }
74
95
 
75
96
  /**
@@ -50,8 +50,6 @@ export class ImpactAnalyzer {
50
50
 
51
51
  const dependents = this.graph.inEdges.get(current) || [];
52
52
  for (const edge of dependents) {
53
- // Allow 'contains' edges so if a function is changed, the file it belongs to is impacted,
54
- // which then allows traversing 'imports' edges from other files.
55
53
  if (!pathSet.has(edge.from)) {
56
54
  const newPathSet = new Set(pathSet);
57
55
  newPathSet.add(edge.from);
@@ -63,10 +61,27 @@ export class ImpactAnalyzer {
63
61
  });
64
62
  }
65
63
  }
64
+
65
+ // Also traverse to contained nodes (functions, classes, variables) inside the current node.
66
+ // This ensures that if a file is impacted, we also check what's inside it for further impact.
67
+ const contained = this.graph.outEdges.get(current) || [];
68
+ for (const edge of contained) {
69
+ if (edge.type === 'contains' && !pathSet.has(edge.to)) {
70
+ const newPathSet = new Set(pathSet);
71
+ newPathSet.add(edge.to);
72
+ queue.push({
73
+ id: edge.to,
74
+ depth: depth + 1,
75
+ path: [...path, edge.to],
76
+ pathSet: newPathSet,
77
+ });
78
+ }
79
+ }
66
80
  }
67
81
 
68
82
  const impactedIds = Array.from(visited.keys()).filter(id =>
69
- !changedNodeIds.includes(id) && id.startsWith('fn:')
83
+ !changedNodeIds.includes(id) &&
84
+ (id.startsWith('fn:') || id.startsWith('class:') || id.startsWith('var:') || id.startsWith('type:') || id.startsWith('prop:'))
70
85
  );
71
86
 
72
87
  let totalRisk = 0;
@@ -84,9 +99,9 @@ export class ImpactAnalyzer {
84
99
  const node = this.graph.nodes.get(id);
85
100
  let risk = this.riskEngine.scoreNode(id);
86
101
 
87
- // Path reversal for confidence calculation (since BFS walks backwards)
88
- const reversedPaths = context.paths.map(p => [...p].reverse());
89
- const confidence = this.confidenceEngine.calculateNodeAggregatedConfidence(reversedPaths);
102
+ // BFS walks backwards (from changed dependents), so paths are already
103
+ // in forward direction: changed → dependent. No reversal needed.
104
+ const confidence = this.confidenceEngine.calculateNodeAggregatedConfidence(context.paths);
90
105
 
91
106
  // Mikk 2.0 Hybrid Risk: Boost if boundary crossed at depth 1
92
107
  // Check if ANY changed node crosses module boundary (not just first one)
@@ -54,12 +54,82 @@ export function getParser(filePath: string): BaseParser {
54
54
  case '.rs':
55
55
  case '.php':
56
56
  case '.rb':
57
- throw new UnsupportedLanguageError(ext)
57
+ // Tree-sitter parser - dynamically imported to handle missing web-tree-sitter
58
+ return createTreeSitterParser()
58
59
  default:
59
60
  throw new UnsupportedLanguageError(ext)
60
61
  }
61
62
  }
62
63
 
64
+ const _treeSitterParserInstance: BaseParser | null = null
65
+
66
+ const createTreeSitterParser = (): BaseParser => {
67
+ // Return a lazy-loading wrapper that handles missing tree-sitter gracefully
68
+ return new LazyTreeSitterParser()
69
+ }
70
+
71
+ class LazyTreeSitterParser extends BaseParser {
72
+ private parser: any = null
73
+
74
+ async init(): Promise<void> {
75
+ if (this.parser) return
76
+ try {
77
+ const { TreeSitterParser } = await import('./tree-sitter/parser.js')
78
+ this.parser = new TreeSitterParser()
79
+ } catch {
80
+ // web-tree-sitter not available
81
+ }
82
+ }
83
+
84
+ async parse(filePath: string, content: string): Promise<ParsedFile> {
85
+ await this.init()
86
+ if (!this.parser) {
87
+ return this.buildEmptyFile(filePath, content)
88
+ }
89
+ return this.parser.parse(filePath, content)
90
+ }
91
+
92
+ async resolveImports(files: ParsedFile[], projectRoot: string): Promise<ParsedFile[]> {
93
+ await this.init()
94
+ if (!this.parser) return files
95
+ return this.parser.resolveImports(files, projectRoot)
96
+ }
97
+
98
+ getSupportedExtensions(): string[] {
99
+ return ['.py', '.java', '.c', '.h', '.cpp', '.cc', '.hpp', '.cs', '.rs', '.php', '.rb']
100
+ }
101
+
102
+ private buildEmptyFile(filePath: string, content: string): ParsedFile {
103
+ const ext = nodePath.extname(filePath).toLowerCase()
104
+ let lang: ParsedFile['language'] = 'unknown'
105
+ switch (ext) {
106
+ case '.py': lang = 'python'; break
107
+ case '.java': lang = 'java'; break
108
+ case '.c': case '.h': lang = 'c'; break
109
+ case '.cpp': case '.cc': case '.hpp': lang = 'cpp'; break
110
+ case '.cs': lang = 'csharp'; break
111
+ case '.go': lang = 'go'; break
112
+ case '.rs': lang = 'rust'; break
113
+ case '.php': lang = 'php'; break
114
+ case '.rb': lang = 'ruby'; break
115
+ }
116
+ return {
117
+ path: filePath,
118
+ language: lang,
119
+ functions: [],
120
+ classes: [],
121
+ generics: [],
122
+ imports: [],
123
+ exports: [],
124
+ routes: [],
125
+ variables: [],
126
+ calls: [],
127
+ hash: '',
128
+ parsedAt: Date.now(),
129
+ }
130
+ }
131
+ }
132
+
63
133
  /**
64
134
  * Parse multiple files, resolve their imports, and return ParsedFile[].
65
135
  *
@@ -62,20 +62,48 @@ export class OxcResolver {
62
62
 
63
63
  const result = this.resolver.sync(dir, source);
64
64
 
65
- if (!result?.path) return '';
65
+ if (!result?.path) {
66
+ return this.fallbackResolve(source, absFrom);
67
+ }
66
68
 
67
69
  const resolved = result.path.replace(/\\/g, '/');
68
70
 
69
- // Only include files within our project root in the graph.
70
- // node_modules, hoisted workspace deps, etc. are external.
71
71
  if (!resolved.startsWith(this.normalizedRoot + '/') && resolved !== this.normalizedRoot) {
72
72
  return '';
73
73
  }
74
74
 
75
75
  return resolved;
76
76
  } catch {
77
+ return this.fallbackResolve(source, fromFile);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Fallback resolution when oxc-resolver fails.
83
+ * Tries common patterns: ./file, ../file, index files, etc.
84
+ */
85
+ private fallbackResolve(source: string, fromFile: string): string {
86
+ if (!source || source.startsWith('node:') || source.startsWith('@')) {
77
87
  return '';
78
88
  }
89
+
90
+ const absFrom = path.isAbsolute(fromFile) ? fromFile : path.resolve(this.projectRoot, fromFile);
91
+ const baseDir = path.dirname(absFrom);
92
+
93
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js', '/index.jsx'];
94
+
95
+ for (const ext of extensions) {
96
+ const candidate = source.endsWith(ext) ? source : source + ext;
97
+ const resolved = path.resolve(baseDir, candidate).replace(/\\/g, '/');
98
+
99
+ if (fs.existsSync(resolved)) {
100
+ if (resolved.startsWith(this.normalizedRoot + '/') || resolved === this.normalizedRoot) {
101
+ return resolved;
102
+ }
103
+ }
104
+ }
105
+
106
+ return '';
79
107
  }
80
108
 
81
109