@getmikk/core 2.0.10 → 2.0.11
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 +9 -7
- package/src/graph/confidence-engine.ts +41 -20
- package/src/graph/impact-analyzer.ts +21 -6
- package/src/parser/index.ts +71 -1
- package/src/parser/oxc-resolver.ts +31 -3
- package/src/parser/tree-sitter/parser.ts +369 -150
- package/src/parser/tree-sitter/queries.ts +102 -16
- package/src/parser/tree-sitter/resolver.ts +261 -0
- package/src/search/bm25.ts +16 -2
- package/src/utils/fs.ts +31 -1
- package/tests/fixtures/python-service/src/auth.py +36 -0
- package/tests/graph.test.ts +2 -1
- package/tests/js-parser.test.ts +444 -0
- package/tests/parser.test.ts +718 -184
- package/tests/tree-sitter-parser.test.ts +827 -130
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikk/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
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
|
-
"
|
|
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
|
}
|
|
@@ -33,43 +33,64 @@ export class ConfidenceEngine {
|
|
|
33
33
|
const current = pathIds[i]
|
|
34
34
|
const next = pathIds[i + 1]
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
54
|
-
const
|
|
55
|
-
for (const edge of
|
|
56
|
-
if (edge.from === current
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
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) &&
|
|
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
|
-
//
|
|
88
|
-
|
|
89
|
-
const confidence = this.confidenceEngine.calculateNodeAggregatedConfidence(
|
|
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)
|
package/src/parser/index.ts
CHANGED
|
@@ -54,12 +54,82 @@ export function getParser(filePath: string): BaseParser {
|
|
|
54
54
|
case '.rs':
|
|
55
55
|
case '.php':
|
|
56
56
|
case '.rb':
|
|
57
|
-
|
|
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)
|
|
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
|
|