@getmikk/core 1.8.3 → 2.0.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 (42) hide show
  1. package/package.json +6 -4
  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 +30 -5
  7. package/src/contract/schema.ts +21 -0
  8. package/src/error-handler.ts +432 -0
  9. package/src/graph/cluster-detector.ts +52 -22
  10. package/src/graph/confidence-engine.ts +85 -0
  11. package/src/graph/graph-builder.ts +298 -255
  12. package/src/graph/impact-analyzer.ts +132 -119
  13. package/src/graph/index.ts +4 -0
  14. package/src/graph/memory-manager.ts +186 -0
  15. package/src/graph/query-engine.ts +76 -0
  16. package/src/graph/risk-engine.ts +86 -0
  17. package/src/graph/types.ts +89 -65
  18. package/src/index.ts +2 -0
  19. package/src/parser/change-detector.ts +99 -0
  20. package/src/parser/go/go-extractor.ts +18 -8
  21. package/src/parser/go/go-parser.ts +2 -0
  22. package/src/parser/index.ts +86 -36
  23. package/src/parser/javascript/js-extractor.ts +1 -1
  24. package/src/parser/javascript/js-parser.ts +2 -0
  25. package/src/parser/oxc-parser.ts +708 -0
  26. package/src/parser/oxc-resolver.ts +83 -0
  27. package/src/parser/tree-sitter/parser.ts +19 -10
  28. package/src/parser/types.ts +100 -73
  29. package/src/parser/typescript/ts-extractor.ts +229 -589
  30. package/src/parser/typescript/ts-parser.ts +16 -171
  31. package/src/parser/typescript/ts-resolver.ts +11 -1
  32. package/src/search/bm25.ts +16 -4
  33. package/src/utils/minimatch.ts +1 -1
  34. package/tests/contract.test.ts +2 -2
  35. package/tests/dead-code.test.ts +7 -7
  36. package/tests/esm-resolver.test.ts +75 -0
  37. package/tests/graph.test.ts +20 -20
  38. package/tests/helpers.ts +11 -6
  39. package/tests/impact-classified.test.ts +37 -41
  40. package/tests/parser.test.ts +7 -5
  41. package/tests/ts-parser.test.ts +27 -52
  42. package/test-output.txt +0 -373
@@ -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
  }
package/src/index.ts CHANGED
@@ -8,6 +8,8 @@ export * from './hash/index.js'
8
8
  export * from './search/index.js'
9
9
  export * from './utils/errors.js'
10
10
  export * from './utils/logger.js'
11
+ export { MikkError, ErrorHandler, ErrorBuilder, ErrorCategory, FileSystemError, ModuleLoadError, GraphError, TokenBudgetError, ValidationError, createDefaultErrorListener, createFileNotFoundError, createFileTooLargeError, createPermissionDeniedError, createModuleNotFoundError, createModuleLoadFailedError, createGraphBuildFailedError, createNodeNotFoundError, createTokenBudgetExceededError, createValidationError, isMikkError, getRootCause, toMikkError } from './error-handler.js'
12
+ export type { } from './error-handler.js'
11
13
  export { discoverFiles, discoverContextFiles, readFileContent, writeFileContent, fileExists, setupMikkDirectory, readMikkIgnore, parseMikkIgnore, detectProjectLanguage, getDiscoveryPatterns, generateMikkIgnore, updateGitIgnore, cleanupGitIgnore } from './utils/fs.js'
12
14
  export type { ContextFile, ContextFileType, ProjectLanguage } from './utils/fs.js'
13
15
  export { minimatch } from './utils/minimatch.js'
@@ -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
  }
@@ -1,12 +1,22 @@
1
- import * as path from 'node:path'
1
+ import * as nodePath from 'node:path'
2
2
  import { BaseParser } from './base-parser.js'
3
- import { TypeScriptParser } from './typescript/ts-parser.js'
3
+ import { OxcParser } from './oxc-parser.js'
4
4
  import { GoParser } from './go/go-parser.js'
5
- import { JavaScriptParser } from './javascript/js-parser.js'
6
5
  import { UnsupportedLanguageError } from '../utils/errors.js'
7
6
  import type { ParsedFile } from './types.js'
8
7
 
9
- export type { ParsedFile, ParsedFunction, ParsedImport, ParsedExport, ParsedClass, ParsedParam } from './types.js'
8
+ export type {
9
+ ParsedFile,
10
+ ParsedFunction,
11
+ ParsedImport,
12
+ ParsedExport,
13
+ ParsedClass,
14
+ ParsedParam,
15
+ ParsedVariable,
16
+ CallExpression,
17
+ ParsedGeneric,
18
+ ParsedRoute
19
+ } from './types.js'
10
20
  export { BaseParser } from './base-parser.js'
11
21
  export { TypeScriptParser } from './typescript/ts-parser.js'
12
22
  export { TypeScriptExtractor } from './typescript/ts-extractor.js'
@@ -19,22 +29,20 @@ export { JavaScriptExtractor } from './javascript/js-extractor.js'
19
29
  export { JavaScriptResolver } from './javascript/js-resolver.js'
20
30
  export { BoundaryChecker } from './boundary-checker.js'
21
31
  export { TreeSitterParser } from './tree-sitter/parser.js'
22
- import { TreeSitterParser } from './tree-sitter/parser.js'
23
32
 
24
33
  /** Get the appropriate parser for a file based on its extension */
25
34
  export function getParser(filePath: string): BaseParser {
26
- const ext = path.extname(filePath)
35
+ const ext = nodePath.extname(filePath).toLowerCase()
27
36
  switch (ext) {
28
37
  case '.ts':
29
38
  case '.tsx':
30
- return new TypeScriptParser()
31
39
  case '.js':
32
40
  case '.mjs':
33
41
  case '.cjs':
34
42
  case '.jsx':
35
- return new JavaScriptParser()
43
+ return new OxcParser()
36
44
  case '.go':
37
- return new GoParser() // Mikk's custom Regex Go parser
45
+ return new GoParser()
38
46
  case '.py':
39
47
  case '.java':
40
48
  case '.c':
@@ -46,55 +54,97 @@ export function getParser(filePath: string): BaseParser {
46
54
  case '.rs':
47
55
  case '.php':
48
56
  case '.rb':
49
- return new TreeSitterParser()
57
+ throw new UnsupportedLanguageError(ext)
50
58
  default:
51
59
  throw new UnsupportedLanguageError(ext)
52
60
  }
53
61
  }
54
62
 
55
- /** Parse multiple files and resolve imports across them */
63
+ /**
64
+ * Parse multiple files, resolve their imports, and return ParsedFile[].
65
+ *
66
+ * Path contract (critical for graph correctness):
67
+ * - filePaths come from discoverFiles() as project-root-relative strings
68
+ * - We resolve them to ABSOLUTE posix paths before passing to parse()
69
+ * - ParsedFile.path is therefore always absolute + forward-slash
70
+ * - OxcResolver also returns absolute paths → import edges always consistent
71
+ */
56
72
  export async function parseFiles(
57
73
  filePaths: string[],
58
74
  projectRoot: string,
59
75
  readFile: (fp: string) => Promise<string>
60
76
  ): Promise<ParsedFile[]> {
61
- const parsersMap = new Map<BaseParser, ParsedFile[]>()
62
- // Re-use parser instances so they can share cache/bindings
63
- const tsParser = new TypeScriptParser()
64
- const jsParser = new JavaScriptParser()
77
+ // Shared parser instances avoid re-initialisation overhead per file
78
+ const oxcParser = new OxcParser()
65
79
  const goParser = new GoParser()
66
- const treeSitterParser = new TreeSitterParser()
67
80
 
68
- const getCachedParser = (ext: string): BaseParser | null => {
69
- switch (ext) {
70
- case '.ts': case '.tsx': return tsParser
71
- case '.js': case '.mjs': case '.cjs': case '.jsx': return jsParser
72
- case '.go': return goParser
73
- case '.py': case '.java': case '.c': case '.h': case '.cpp': case '.cc': case '.hpp': case '.cs': case '.rs': case '.php': case '.rb': return treeSitterParser
74
- default: return null
81
+ // Lazily loaded to avoid mandatory dep on tree-sitter
82
+ let treeSitterParser: BaseParser | null = null
83
+ const getTreeSitter = async (): Promise<BaseParser> => {
84
+ if (!treeSitterParser) {
85
+ const { TreeSitterParser } = await import('./tree-sitter/parser.js')
86
+ treeSitterParser = new TreeSitterParser()
75
87
  }
88
+ return treeSitterParser!
76
89
  }
77
90
 
91
+ const tsExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'])
92
+ const goExtensions = new Set(['.go'])
93
+ const treeSitterExtensions = new Set(['.py', '.java', '.c', '.h', '.cpp', '.cc', '.hpp', '.cs', '.rs', '.php', '.rb'])
94
+
95
+ // Normalised project root for absolute path construction
96
+ const normalizedRoot = nodePath.resolve(projectRoot).replace(/\\/g, '/')
97
+
98
+ // Group by parser to enable batch resolveImports
99
+ const oxcFiles: ParsedFile[] = []
100
+ const goFiles: ParsedFile[] = []
101
+ const treeFiles: ParsedFile[] = []
102
+
103
+ // Parse sequentially to avoid races in parser implementations that keep
104
+ // mutable per-instance state (e.g. language switching/counters).
78
105
  for (const fp of filePaths) {
79
- const ext = path.extname(fp).toLowerCase()
80
- const parser = getCachedParser(ext)
81
- if (!parser) continue
106
+ const ext = nodePath.extname(fp).toLowerCase()
107
+
108
+ // Build absolute posix path — this is the single source of truth for all IDs
109
+ const absoluteFp = nodePath.resolve(normalizedRoot, fp).replace(/\\/g, '/')
82
110
 
111
+ let content: string
83
112
  try {
84
- const content = await readFile(path.join(projectRoot, fp))
85
- const parsed = await parser.parse(fp, content)
86
-
87
- if (!parsersMap.has(parser)) parsersMap.set(parser, [])
88
- parsersMap.get(parser)!.push(parsed)
113
+ content = await readFile(absoluteFp)
89
114
  } catch {
90
- // Skip unreadable files (permissions, binary, etc.) — don't abort the whole parse
115
+ // File unreadable skip silently (deleted, permission error, binary)
116
+ continue
117
+ }
118
+
119
+ try {
120
+ if (tsExtensions.has(ext)) {
121
+ const parsed = await oxcParser.parse(absoluteFp, content)
122
+ oxcFiles.push(parsed)
123
+ } else if (goExtensions.has(ext)) {
124
+ const parsed = await goParser.parse(absoluteFp, content)
125
+ goFiles.push(parsed)
126
+ } else if (treeSitterExtensions.has(ext)) {
127
+ const ts = await getTreeSitter()
128
+ const parsed = await ts.parse(absoluteFp, content)
129
+ treeFiles.push(parsed)
130
+ }
131
+ } catch {
132
+ // Parser error — skip this file, don't abort the whole run
91
133
  }
92
134
  }
93
135
 
94
- const allResolvedFiles: ParsedFile[] = []
95
- for (const [parser, files] of parsersMap.entries()) {
96
- allResolvedFiles.push(...parser.resolveImports(files, projectRoot))
136
+ // Resolve imports batch-wise per parser (each has its own resolver)
137
+ let resolvedTreeFiles: ParsedFile[] = treeFiles
138
+ if (treeFiles.length > 0) {
139
+ const treeParser = treeSitterParser ?? await getTreeSitter()
140
+ resolvedTreeFiles = treeParser.resolveImports(treeFiles, normalizedRoot)
97
141
  }
98
142
 
99
- return allResolvedFiles
143
+ const resolved: ParsedFile[] = [
144
+ ...oxcParser.resolveImports(oxcFiles, normalizedRoot),
145
+ ...goParser.resolveImports(goFiles, normalizedRoot),
146
+ ...resolvedTreeFiles,
147
+ ]
148
+
149
+ return resolved
100
150
  }
@@ -231,7 +231,7 @@ export class JavaScriptExtractor extends TypeScriptExtractor {
231
231
  returnType: rhs.type ? rhs.type.getText(this.sourceFile) : 'void',
232
232
  isExported: true,
233
233
  isAsync,
234
- calls: this.extractCalls(rhs),
234
+ calls: this.extractCallsFromNode(rhs),
235
235
  hash: hashContent(rhs.getText(this.sourceFile)),
236
236
  purpose: this.extractPurpose(node),
237
237
  edgeCasesHandled: this.extractEdgeCases(rhs),
@@ -48,6 +48,8 @@ export class JavaScriptParser extends BaseParser {
48
48
  imports,
49
49
  exports,
50
50
  routes,
51
+ variables: [],
52
+ calls: [],
51
53
  hash: hashContent(content),
52
54
  parsedAt: Date.now(),
53
55
  }