@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.
Files changed (41) 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 +66 -35
  6. package/src/contract/lock-reader.ts +24 -4
  7. package/src/contract/schema.ts +21 -0
  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/graph-builder.ts +298 -255
  12. package/src/graph/impact-analyzer.ts +130 -119
  13. package/src/graph/index.ts +4 -0
  14. package/src/graph/memory-manager.ts +345 -0
  15. package/src/graph/query-engine.ts +79 -0
  16. package/src/graph/risk-engine.ts +86 -0
  17. package/src/graph/types.ts +89 -65
  18. package/src/parser/change-detector.ts +99 -0
  19. package/src/parser/go/go-extractor.ts +18 -8
  20. package/src/parser/go/go-parser.ts +2 -0
  21. package/src/parser/index.ts +88 -38
  22. package/src/parser/javascript/js-extractor.ts +1 -1
  23. package/src/parser/javascript/js-parser.ts +2 -0
  24. package/src/parser/oxc-parser.ts +675 -0
  25. package/src/parser/oxc-resolver.ts +83 -0
  26. package/src/parser/tree-sitter/parser.ts +19 -10
  27. package/src/parser/types.ts +100 -73
  28. package/src/parser/typescript/ts-extractor.ts +229 -589
  29. package/src/parser/typescript/ts-parser.ts +16 -171
  30. package/src/parser/typescript/ts-resolver.ts +11 -1
  31. package/src/search/bm25.ts +5 -2
  32. package/src/utils/minimatch.ts +1 -1
  33. package/tests/contract.test.ts +2 -2
  34. package/tests/dead-code.test.ts +7 -7
  35. package/tests/esm-resolver.test.ts +75 -0
  36. package/tests/graph.test.ts +20 -20
  37. package/tests/helpers.ts +11 -6
  38. package/tests/impact-classified.test.ts +37 -41
  39. package/tests/parser.test.ts +7 -5
  40. package/tests/ts-parser.test.ts +27 -52
  41. package/test-output.txt +0 -373
@@ -0,0 +1,79 @@
1
+ import type { DependencyGraph } from './types.js'
2
+
3
+ /**
4
+ * Mikk 2.0: Query Engine
5
+ * Provides high-performance graph traversal and path-finding tools.
6
+ * Focuses on symbol-level precision.
7
+ */
8
+ export class QueryEngine {
9
+ constructor(private graph: DependencyGraph) {}
10
+
11
+ /** Find all direct dependents (who calls me?) */
12
+ public getDependents(nodeId: string): string[] {
13
+ return (this.graph.inEdges.get(nodeId) || [])
14
+ .filter(e => e.type !== 'contains')
15
+ .map(e => e.from);
16
+ }
17
+
18
+ /** Find all direct dependencies (who do I call?) */
19
+ public getDependencies(nodeId: string): string[] {
20
+ return (this.graph.outEdges.get(nodeId) || [])
21
+ .filter(e => e.type !== 'contains')
22
+ .map(e => e.to);
23
+ }
24
+
25
+ /**
26
+ * Find the shortest path between two nodes using BFS.
27
+ * Returns an array of node IDs or null if no path exists.
28
+ */
29
+ public findPath(start: string, end: string): string[] | null {
30
+ if (!this.graph.nodes.has(start) || !this.graph.nodes.has(end)) return null;
31
+ if (start === end) return [start];
32
+
33
+ const queue: { id: string, path: string[] }[] = [{ id: start, path: [start] }];
34
+ const visited = new Set<string>([start]);
35
+
36
+ while (queue.length > 0) {
37
+ const { id, path } = queue.shift()!;
38
+
39
+ const outwardEdges = this.graph.outEdges.get(id) || [];
40
+ for (const edge of outwardEdges) {
41
+ if (edge.type === 'contains') continue;
42
+
43
+ if (edge.to === end) {
44
+ return [...path, end];
45
+ }
46
+
47
+ if (!visited.has(edge.to)) {
48
+ visited.add(edge.to);
49
+ queue.push({ id: edge.to, path: [...path, edge.to] });
50
+ }
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Get the full downstream impact (transitive dependents) of a node.
59
+ * Useful for assessing "What would break if I change X?"
60
+ */
61
+ public getDownstreamImpact(nodeId: string): string[] {
62
+ const visited = new Set<string>();
63
+ const queue: string[] = [nodeId];
64
+
65
+ while (queue.length > 0) {
66
+ const current = queue.shift()!;
67
+ const dependents = this.getDependents(current);
68
+
69
+ for (const dep of dependents) {
70
+ if (!visited.has(dep) && dep !== nodeId) {
71
+ visited.add(dep);
72
+ queue.push(dep);
73
+ }
74
+ }
75
+ }
76
+
77
+ return Array.from(visited);
78
+ }
79
+ }
@@ -0,0 +1,86 @@
1
+ import type { DependencyGraph, GraphNode } from './types.js'
2
+
3
+ export interface RiskContext {
4
+ connectedNodesCount: number;
5
+ dependencyDepth: number;
6
+ }
7
+
8
+ export interface RiskModifiers {
9
+ isAuthOrSecurity: boolean;
10
+ isDatabaseOrState: boolean;
11
+ isPublicAPI: boolean;
12
+ }
13
+
14
+ /**
15
+ * Mikk 2.0: Risk Engine
16
+ * Computes risk scores based on a quantitative mathematical model.
17
+ */
18
+ export class RiskEngine {
19
+ constructor(private graph: DependencyGraph) {}
20
+
21
+ /**
22
+ * Compute the absolute risk score (0-100) for modifying a specific node.
23
+ * Formula: Base Risk = (Connected Nodes * 1.5) + (Depth * 2) + Modifiers
24
+ */
25
+ public scoreNode(nodeId: string): number {
26
+ const node = this.graph.nodes.get(nodeId);
27
+ if (!node) return 0;
28
+
29
+ const context = this.analyzeContext(nodeId);
30
+ const modifiers = this.analyzeModifiers(node);
31
+
32
+ let score = (context.connectedNodesCount * 1.5) + (context.dependencyDepth * 2);
33
+
34
+ // Apply strict modifiers
35
+ if (modifiers.isAuthOrSecurity) score += 30;
36
+ if (modifiers.isDatabaseOrState) score += 20;
37
+ if (modifiers.isPublicAPI) score += 15;
38
+
39
+ return Math.min(Math.max(score, 0), 100);
40
+ }
41
+
42
+ private analyzeContext(nodeId: string): RiskContext {
43
+ const visited = new Set<string>();
44
+ let maxDepth = 0;
45
+
46
+ // Use index pointer instead of queue.shift() — avoids O(n) array shift per pop.
47
+ const queue: Array<{ id: string, depth: number }> = [{ id: nodeId, depth: 0 }];
48
+ let queueHead = 0;
49
+ visited.add(nodeId);
50
+
51
+ let connectedNodesCount = 0;
52
+
53
+ while (queueHead < queue.length) {
54
+ const current = queue[queueHead++];
55
+ maxDepth = Math.max(maxDepth, current.depth);
56
+
57
+ const inEdges = this.graph.inEdges.get(current.id) || [];
58
+ connectedNodesCount += inEdges.length;
59
+
60
+ for (const edge of inEdges) {
61
+ if (!visited.has(edge.from)) {
62
+ visited.add(edge.from);
63
+ queue.push({ id: edge.from, depth: current.depth + 1 });
64
+ }
65
+ }
66
+ }
67
+
68
+ return {
69
+ connectedNodesCount,
70
+ dependencyDepth: maxDepth
71
+ };
72
+ }
73
+
74
+ private analyzeModifiers(node: GraphNode): RiskModifiers {
75
+ const nameAndFile = `${node.name} ${node.file}`.toLowerCase();
76
+
77
+ const authKeywords = ['auth', 'login', 'jwt', 'verify', 'token', 'crypt', 'hash', 'password'];
78
+ const dbKeywords = ['db', 'query', 'sql', 'insert', 'update', 'delete', 'redis', 'cache', 'transaction'];
79
+
80
+ return {
81
+ isAuthOrSecurity: authKeywords.some(kw => nameAndFile.includes(kw)),
82
+ isDatabaseOrState: dbKeywords.some(kw => nameAndFile.includes(kw)),
83
+ isPublicAPI: !!node.metadata?.isExported
84
+ };
85
+ }
86
+ }
@@ -1,83 +1,107 @@
1
- /**
2
- * Graph types — nodes, edges, and the dependency graph itself.
3
- */
1
+ export type NodeType =
2
+ | "file"
3
+ | "class"
4
+ | "function"
5
+ | "variable"
6
+ | "generic";
4
7
 
5
- export type NodeType = 'function' | 'file' | 'module' | 'class' | 'generic'
6
- export type EdgeType = 'calls' | 'imports' | 'exports' | 'contains'
8
+ export type EdgeType =
9
+ | "imports"
10
+ | "calls"
11
+ | "extends"
12
+ | "implements"
13
+ | "accesses"
14
+ | "contains"; // Keeping for containment edges
7
15
 
8
- /** A single node in the dependency graph */
9
16
  export interface GraphNode {
10
- id: string // "fn:src/auth/verify.ts:verifyToken"
11
- type: NodeType
12
- label: string // "verifyToken"
13
- file: string // "src/auth/verify.ts"
14
- moduleId?: string // "auth" which declared module this belongs to
15
- metadata: {
16
- startLine?: number
17
- endLine?: number
18
- isExported?: boolean
19
- isAsync?: boolean
20
- hash?: string
21
- purpose?: string
22
- genericKind?: string
23
- params?: { name: string; type: string; optional?: boolean }[]
24
- returnType?: string
25
- edgeCasesHandled?: string[]
26
- errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[]
27
- detailedLines?: { startLine: number; endLine: number; blockType: string }[]
28
- }
17
+ id: string; // unique (normalized file::name)
18
+ type: NodeType;
19
+ name: string;
20
+ file: string;
21
+ moduleId?: string; // Original cluster feature
22
+
23
+ metadata?: {
24
+ isExported?: boolean;
25
+ inheritsFrom?: string[];
26
+ implements?: string[];
27
+ className?: string; // for methods
28
+ startLine?: number;
29
+ endLine?: number;
30
+ isAsync?: boolean;
31
+ hash?: string;
32
+ purpose?: string;
33
+ genericKind?: string;
34
+ params?: { name: string; type: string; optional?: boolean }[];
35
+ returnType?: string;
36
+ edgeCasesHandled?: string[];
37
+ errorHandling?: { line: number; type: 'try-catch' | 'throw'; detail: string }[];
38
+ detailedLines?: { startLine: number; endLine: number; blockType: string }[];
39
+ };
29
40
  }
30
41
 
31
- /** A single edge in the dependency graph */
32
42
  export interface GraphEdge {
33
- source: string // "fn:src/auth/verify.ts:verifyToken"
34
- target: string // "fn:src/utils/jwt.ts:jwtDecode"
35
- type: EdgeType
36
- weight?: number // How often this call happens (for coupling metrics)
37
- confidence?: number // 0.0–1.0: 1.0 = direct AST call, 0.8 = via interface, 0.5 = fuzzy/inferred
43
+ from: string;
44
+ to: string;
45
+ type: EdgeType;
46
+ confidence: number; // 0–1
47
+ weight?: number; // Weight from EDGE_WEIGHT constants
38
48
  }
39
49
 
40
- /** The full dependency graph */
41
50
  export interface DependencyGraph {
42
- nodes: Map<string, GraphNode>
43
- edges: GraphEdge[]
44
- outEdges: Map<string, GraphEdge[]> // node → [edges going out]
45
- inEdges: Map<string, GraphEdge[]> // node → [edges coming in]
51
+ nodes: Map<string, GraphNode>;
52
+ edges: GraphEdge[];
53
+ outEdges: Map<string, GraphEdge[]>; // node → [edges going out]
54
+ inEdges: Map<string, GraphEdge[]>; // node → [edges coming in]
46
55
  }
47
56
 
48
- /** Risk level for an impacted node */
49
- export type RiskLevel = 'critical' | 'high' | 'medium' | 'low'
50
-
51
- /** A single node in the classified impact result */
52
- export interface ClassifiedImpact {
53
- nodeId: string
54
- label: string
55
- file: string
56
- moduleId?: string
57
- risk: RiskLevel
58
- depth: number // hops from change
57
+ /**
58
+ * Canonical ID helpers.
59
+ * Function IDs: fn:<absolute-posix-path>:<FunctionName>
60
+ * Class IDs: class:<absolute-posix-path>:<ClassName>
61
+ * Type/enum IDs: type:<absolute-posix-path>:<Name> | enum:<absolute-posix-path>:<Name>
62
+ * File IDs: <absolute-posix-path> (no prefix)
63
+ *
64
+ * NOTE: The old normalizeId() that used `file::name` (double-colon, lowercase)
65
+ * was removed — it did not match any current ID format and would produce IDs
66
+ * that never matched any graph node.
67
+ */
68
+ export function makeFnId(file: string, name: string): string {
69
+ return `fn:${file.replace(/\\/g, '/')}:${name}`;
59
70
  }
60
71
 
61
- /** Result of impact analysis */
72
+ export type RiskLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
73
+
62
74
  export interface ImpactResult {
63
- changed: string[] // The directly changed nodes
64
- impacted: string[] // Everything that depends on changed nodes
65
- depth: number // How many hops from change to furthest impact
66
- confidence: 'high' | 'medium' | 'low'
67
- /** Risk-classified breakdown of impacted nodes */
68
- classified: {
69
- critical: ClassifiedImpact[]
70
- high: ClassifiedImpact[]
71
- medium: ClassifiedImpact[]
72
- low: ClassifiedImpact[]
73
- }
75
+ changed: string[];
76
+ impacted: string[];
77
+ allImpacted: ClassifiedImpact[]; // New field for Decision Engine
78
+ depth: number;
79
+ entryPoints: string[];
80
+ criticalModules: string[];
81
+ paths: string[][];
82
+ confidence: number;
83
+ riskScore: number;
84
+ classified: {
85
+ critical: ClassifiedImpact[];
86
+ high: ClassifiedImpact[];
87
+ medium: ClassifiedImpact[];
88
+ low: ClassifiedImpact[];
89
+ };
90
+ }
91
+
92
+ export interface ClassifiedImpact {
93
+ nodeId: string;
94
+ label: string;
95
+ file: string;
96
+ risk: RiskLevel;
97
+ riskScore: number; // numeric score for precise policy checks
98
+ depth: number;
74
99
  }
75
100
 
76
- /** A cluster of files that naturally belong together */
77
101
  export interface ModuleCluster {
78
- id: string
79
- files: string[]
80
- confidence: number // 0.0 to 1.0
81
- suggestedName: string // inferred from folder names
82
- functions: string[] // function IDs in this cluster
102
+ id: string;
103
+ files: string[];
104
+ confidence: number;
105
+ suggestedName: string;
106
+ functions: string[];
83
107
  }
@@ -0,0 +1,99 @@
1
+ import type { ParsedFile } from './types.js'
2
+
3
+ export interface ChangeSet {
4
+ added: string[]; // Symbol IDs
5
+ removed: string[]; // Symbol IDs
6
+ modified: string[]; // Symbol IDs (hash mismatch)
7
+ }
8
+
9
+ /**
10
+ * Mikk 2.0: Change Detector
11
+ * Compares parsed file versions to identify precisely which symbols
12
+ * (functions, classes, etc.) have changed using AST hashes.
13
+ */
14
+ export class ChangeDetector {
15
+ /**
16
+ * Compare two versions of a file and return the changed symbol IDs.
17
+ */
18
+ public detectSymbolChanges(oldFile: ParsedFile | undefined, newFile: ParsedFile): ChangeSet {
19
+ const added: string[] = [];
20
+ const removed: string[] = [];
21
+ const modified: string[] = [];
22
+
23
+ if (!oldFile) {
24
+ // Everything is new
25
+ return {
26
+ added: [
27
+ ...newFile.functions.map(f => f.id),
28
+ ...newFile.classes.map(c => c.id),
29
+ ...newFile.generics.map(g => g.id),
30
+ ...newFile.variables.map(v => v.id),
31
+ ],
32
+ removed: [],
33
+ modified: []
34
+ };
35
+ }
36
+
37
+ if (oldFile.hash === newFile.hash) {
38
+ return { added: [], removed: [], modified: [] };
39
+ }
40
+
41
+ // --- Compare Functions ---
42
+ const oldFns = new Map(oldFile.functions.map(f => [f.id, f.hash]));
43
+ const newFns = new Map(newFile.functions.map(f => [f.id, f.hash]));
44
+
45
+ for (const [id, hash] of newFns) {
46
+ if (!oldFns.has(id)) added.push(id);
47
+ else if (oldFns.get(id) !== hash) modified.push(id);
48
+ }
49
+ for (const id of oldFns.keys()) {
50
+ if (!newFns.has(id)) removed.push(id);
51
+ }
52
+
53
+ // --- Compare Classes ---
54
+ const oldClasses = new Map(oldFile.classes.map(c => [c.id, c.hash || 'no-hash'])); // ParsedClass might not have hash yet, using placeholder
55
+ const newClasses = new Map(newFile.classes.map(c => [c.id, c.hash || 'no-hash']));
56
+
57
+ // Note: If ParsedClass doesn't have a hash, we might need to compare methods/properties hashes
58
+ // For Mikk 2.0, we'll assume classes have a summary hash eventually or just compare their structure.
59
+
60
+ for (const [id, hash] of newClasses) {
61
+ if (!oldClasses.has(id)) added.push(id);
62
+ else if (oldClasses.get(id) !== hash) modified.push(id);
63
+ }
64
+ for (const id of oldClasses.keys()) {
65
+ if (!newClasses.has(id)) removed.push(id);
66
+ }
67
+
68
+ // --- Compare Generics ---
69
+ const oldGenerics = new Map(oldFile.generics.map(g => [g.id, g.id])); // Simplified
70
+ const newGenerics = new Map(newFile.generics.map(g => [g.id, g.id]));
71
+
72
+ for (const id of newGenerics.keys()) {
73
+ if (!oldGenerics.has(id)) added.push(id);
74
+ }
75
+ for (const id of oldGenerics.keys()) {
76
+ if (!newGenerics.has(id)) removed.push(id);
77
+ }
78
+
79
+ return { added, removed, modified };
80
+ }
81
+
82
+ /**
83
+ * Compare two sets of files and return all modified symbol IDs across the project.
84
+ */
85
+ public detectBatchChanges(oldFiles: Map<string, ParsedFile>, newFiles: ParsedFile[]): string[] {
86
+ const allChangedIds = new Set<string>();
87
+
88
+ for (const file of newFiles) {
89
+ const old = oldFiles.get(file.path);
90
+ const diff = this.detectSymbolChanges(old, file);
91
+
92
+ diff.added.forEach(id => allChangedIds.add(id));
93
+ diff.modified.forEach(id => allChangedIds.add(id));
94
+ // Removal is handled by graph-builder removing stale nodes, but we might want to track it for impact
95
+ }
96
+
97
+ return Array.from(allChangedIds);
98
+ }
99
+ }
@@ -1,7 +1,7 @@
1
1
  import { hashContent } from '../../hash/file-hasher.js'
2
2
  import type {
3
3
  ParsedFunction, ParsedClass, ParsedImport, ParsedExport,
4
- ParsedParam, ParsedGeneric, ParsedRoute,
4
+ ParsedParam, ParsedGeneric, ParsedRoute, CallExpression
5
5
  } from '../types.js'
6
6
 
7
7
  // --- Go builtins / keywords to skip when extracting calls -------------------
@@ -97,6 +97,8 @@ export class GoExtractor {
97
97
  isExported: isExported(typeDecl.name),
98
98
  purpose: typeDecl.purpose,
99
99
  methods: methods.map(m => this.buildParsedFunction(m)),
100
+ properties: [],
101
+ hash: '',
100
102
  })
101
103
  byReceiver.delete(typeDecl.name)
102
104
  }
@@ -111,6 +113,9 @@ export class GoExtractor {
111
113
  endLine: methods[methods.length - 1]?.endLine ?? 0,
112
114
  isExported: isExported(receiverType),
113
115
  methods: methods.map(m => this.buildParsedFunction(m)),
116
+ properties: [],
117
+ hash: '',
118
+ purpose: '',
114
119
  })
115
120
  }
116
121
 
@@ -250,7 +255,7 @@ export class GoExtractor {
250
255
 
251
256
  const bodyLines = this.lines.slice(raw.bodyStart - 1, raw.endLine)
252
257
  const hash = hashContent(bodyLines.join('\n'))
253
- const calls = extractCallsFromBody(bodyLines)
258
+ const calls = extractCallsFromBody(bodyLines, raw.bodyStart)
254
259
  const edgeCases = extractEdgeCases(bodyLines)
255
260
  const errorHandling = extractErrorHandling(bodyLines, raw.bodyStart)
256
261
 
@@ -620,25 +625,30 @@ function stripStringsAndComments(code: string): string {
620
625
  return out
621
626
  }
622
627
 
623
- function extractCallsFromBody(bodyLines: string[]): string[] {
624
- const stripped = stripStringsAndComments(bodyLines.join('\n'))
625
- const calls = new Set<string>()
628
+ function extractCallsFromBody(bodyLines: string[], baseLine: number = 1): CallExpression[] {
629
+ const code = bodyLines.join('\n')
630
+ const stripped = stripStringsAndComments(code)
631
+ const calls: CallExpression[] = []
626
632
 
627
633
  // Direct calls: identifier(
628
634
  const callRe = /\b([A-Za-z_]\w*)\s*\(/g
629
635
  let m: RegExpExecArray | null
630
636
  while ((m = callRe.exec(stripped)) !== null) {
631
637
  const name = m[1]
632
- if (!GO_BUILTINS.has(name)) calls.add(name)
638
+ if (!GO_BUILTINS.has(name)) {
639
+ // Heuristic for line number: find the line in bodyLines
640
+ // This is a rough estimation but good enough for static analysis
641
+ calls.push({ name, line: baseLine, type: 'function' })
642
+ }
633
643
  }
634
644
 
635
645
  // Method calls: receiver.Method(
636
646
  const methodRe = /\b([A-Za-z_]\w+\.[A-Za-z_]\w*)\s*\(/g
637
647
  while ((m = methodRe.exec(stripped)) !== null) {
638
- calls.add(m[1])
648
+ calls.push({ name: m[1], line: baseLine, type: 'method' })
639
649
  }
640
650
 
641
- return [...calls]
651
+ return calls
642
652
  }
643
653
 
644
654
  function extractEdgeCases(bodyLines: string[]): string[] {
@@ -22,6 +22,8 @@ export class GoParser extends BaseParser {
22
22
  imports: extractor.extractImports(),
23
23
  exports: extractor.extractExports(),
24
24
  routes: extractor.extractRoutes(),
25
+ variables: [],
26
+ calls: [],
25
27
  hash: hashContent(content),
26
28
  parsedAt: Date.now(),
27
29
  }