@getmikk/core 1.8.3 → 1.9.1

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,191 +1,36 @@
1
- import * as path from 'node:path'
2
- import * as fs from 'node:fs'
3
1
  import { BaseParser } from '../base-parser.js'
2
+ import type { ParsedFile } from '../types.js'
4
3
  import { TypeScriptExtractor } from './ts-extractor.js'
5
4
  import { TypeScriptResolver } from './ts-resolver.js'
6
5
  import { hashContent } from '../../hash/file-hasher.js'
7
- import type { ParsedFile } from '../types.js'
8
- import { MIN_FILES_FOR_COMPLETE_SCAN, parseJsonWithComments } from '../parser-constants.js'
9
6
 
10
- /**
11
- * TypeScript parser — uses TS Compiler API to parse .ts/.tsx files
12
- * and extract structured data (functions, classes, imports, exports).
13
- */
14
7
  export class TypeScriptParser extends BaseParser {
15
- /** Parse a single TypeScript file */
16
- async parse(filePath: string, content: string): Promise<ParsedFile> {
8
+ public async parse(filePath: string, content: string): Promise<ParsedFile> {
17
9
  const extractor = new TypeScriptExtractor(filePath, content)
18
- const functions = extractor.extractFunctions()
19
- const classes = extractor.extractClasses()
20
- const generics = extractor.extractGenerics()
21
- const imports = extractor.extractImports()
22
- const exports = extractor.extractExports()
23
- const routes = extractor.extractRoutes()
24
-
25
- // Cross-reference: re-export declarations (`export { Name }` or
26
- // `export { X as Y } from './m'`) may refer to symbols whose declaration
27
- // doesn't carry an export keyword. Mark them as exported here.
28
- // Exclude `type: 'default'` to avoid marking an unrelated local called 'default'.
29
- const exportedNonDefault = new Set(
30
- exports.filter(e => e.type !== 'default').map(e => e.name)
31
- )
32
- for (const fn of functions) {
33
- if (!fn.isExported && exportedNonDefault.has(fn.name)) fn.isExported = true
34
- }
35
- for (const cls of classes) {
36
- if (!cls.isExported && exportedNonDefault.has(cls.name)) cls.isExported = true
37
- }
38
- for (const gen of generics) {
39
- if (!gen.isExported && exportedNonDefault.has(gen.name)) gen.isExported = true
40
- }
41
-
10
+
42
11
  return {
43
12
  path: filePath,
44
13
  language: 'typescript',
45
- functions,
46
- classes,
47
- generics,
48
- imports,
49
- exports,
50
- routes,
14
+ functions: extractor.extractFunctions(),
15
+ classes: extractor.extractClasses(),
16
+ variables: extractor.extractVariables(),
17
+ generics: extractor.extractGenerics(),
18
+ imports: extractor.extractImports(),
19
+ exports: extractor.extractExports(),
20
+ routes: extractor.extractRoutes(),
21
+ calls: extractor.extractModuleCalls(),
51
22
  hash: hashContent(content),
52
- parsedAt: Date.now(),
23
+ parsedAt: Date.now()
53
24
  }
54
25
  }
55
26
 
56
- /** Resolve all import paths in parsed files to absolute project paths */
57
- resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
58
- const tsConfigPaths = loadTsConfigPaths(projectRoot)
59
- const resolver = new TypeScriptResolver(projectRoot, tsConfigPaths)
60
-
61
- // Only pass the project file list when it is large enough to be a meaningful
62
- // scan. Sparse lists (< MIN_FILES_FOR_COMPLETE_SCAN files) cause alias
63
- // resolution lookups to fail with '', so we only trust the list once it is
64
- // sufficiently large. With an empty list the resolver falls back to extension
65
- // probing, which is safe for alias-defined paths.
66
- const allFilePaths = files.length >= MIN_FILES_FOR_COMPLETE_SCAN ? files.map(f => f.path) : []
67
-
68
- return files.map(file => ({
69
- ...file,
70
- imports: file.imports.map(imp => resolver.resolve(imp, file.path, allFilePaths)),
71
- }))
27
+ public resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
28
+ const resolver = new TypeScriptResolver(projectRoot)
29
+ return resolver.resolveBatch(files)
72
30
  }
73
31
 
74
- getSupportedExtensions(): string[] {
32
+ public getSupportedExtensions(): string[] {
75
33
  return ['.ts', '.tsx']
76
34
  }
77
35
  }
78
36
 
79
- /**
80
- * Read compilerOptions.paths from tsconfig.json in projectRoot.
81
- * Recursively follows "extends" chains (e.g. extends ./tsconfig.base.json,
82
- * extends @tsconfig/node-lts/tsconfig.json) and merges paths.
83
- *
84
- * Handles:
85
- * - extends with relative paths (./tsconfig.base.json)
86
- * - extends with node_modules packages (@tsconfig/node-lts)
87
- * - baseUrl prefix so aliases like "@/*" → ["src/*"] resolve correctly
88
- * - JSON5-style comments (line and block comments) via the shared helper
89
- */
90
- function loadTsConfigPaths(projectRoot: string): Record<string, string[]> {
91
- const candidates = ['tsconfig.json', 'tsconfig.base.json']
92
- for (const name of candidates) {
93
- const tsConfigPath = path.join(projectRoot, name)
94
- try {
95
- const merged = loadTsConfigWithExtends(tsConfigPath, new Set())
96
- const options = merged.compilerOptions ?? {}
97
- const rawPaths: Record<string, string[]> = options.paths ?? {}
98
- if (Object.keys(rawPaths).length === 0) continue
99
-
100
- const baseUrl: string = options.baseUrl ?? '.'
101
- const resolved: Record<string, string[]> = {}
102
- for (const [alias, targets] of Object.entries(rawPaths)) {
103
- resolved[alias] = (targets as string[]).map(t =>
104
- t.startsWith('.') ? path.posix.join(baseUrl, t) : t
105
- )
106
- }
107
- return resolved
108
- } catch { /* tsconfig not found or invalid — continue */ }
109
- }
110
- return {}
111
- }
112
-
113
- /**
114
- * Recursively load a tsconfig, following the "extends" chain.
115
- * Merges compilerOptions from parent → child (child wins on conflict).
116
- * Prevents infinite loops via a visited set.
117
- */
118
- function loadTsConfigWithExtends(configPath: string, visited: Set<string>): any {
119
- const resolved = path.resolve(configPath)
120
- if (visited.has(resolved)) return {}
121
- visited.add(resolved)
122
-
123
- let raw: string
124
- try {
125
- raw = fs.readFileSync(resolved, 'utf-8')
126
- } catch {
127
- return {}
128
- }
129
-
130
- let config: any
131
- try {
132
- config = parseJsonWithComments(raw)
133
- } catch {
134
- return {}
135
- }
136
-
137
- if (!config.extends) return config
138
-
139
- // Resolve the parent config path
140
- const extendsValue = config.extends
141
- let parentPath: string
142
-
143
- if (extendsValue.startsWith('.')) {
144
- // Relative path: ./tsconfig.base.json or ../tsconfig.json
145
- parentPath = path.resolve(path.dirname(resolved), extendsValue)
146
- // Add .json if missing
147
- if (!parentPath.endsWith('.json')) parentPath += '.json'
148
- } else {
149
- // Node module: @tsconfig/node-lts or @tsconfig/node-lts/tsconfig.json
150
- try {
151
- // Try resolving as a node module from projectRoot
152
- const projectRoot = path.dirname(resolved)
153
- const modulePath = path.join(projectRoot, 'node_modules', extendsValue)
154
- if (fs.existsSync(modulePath + '.json')) {
155
- parentPath = modulePath + '.json'
156
- } else if (fs.existsSync(path.join(modulePath, 'tsconfig.json'))) {
157
- parentPath = path.join(modulePath, 'tsconfig.json')
158
- } else if (fs.existsSync(modulePath)) {
159
- parentPath = modulePath
160
- } else {
161
- // Can't resolve — skip extends
162
- delete config.extends
163
- return config
164
- }
165
- } catch {
166
- delete config.extends
167
- return config
168
- }
169
- }
170
-
171
- // Load parent recursively
172
- const parent = loadTsConfigWithExtends(parentPath, visited)
173
-
174
- // Merge: parent compilerOptions → child compilerOptions (child wins)
175
- const merged = { ...config }
176
- delete merged.extends
177
- merged.compilerOptions = {
178
- ...(parent.compilerOptions ?? {}),
179
- ...(config.compilerOptions ?? {}),
180
- }
181
-
182
- // Merge paths specifically (child paths override parent paths for same alias)
183
- if (parent.compilerOptions?.paths || config.compilerOptions?.paths) {
184
- merged.compilerOptions.paths = {
185
- ...(parent.compilerOptions?.paths ?? {}),
186
- ...(config.compilerOptions?.paths ?? {}),
187
- }
188
- }
189
-
190
- return merged
191
- }
@@ -1,5 +1,5 @@
1
1
  import * as path from 'node:path'
2
- import type { ParsedImport } from '../types.js'
2
+ import type { ParsedImport, ParsedFile } from '../types.js'
3
3
 
4
4
  interface TSConfigPaths {
5
5
  [alias: string]: string[]
@@ -26,6 +26,16 @@ export class TypeScriptResolver {
26
26
  this.aliases = tsConfigPaths ?? {}
27
27
  }
28
28
 
29
+ /** Resolve all imports for a batch of files */
30
+ public resolveBatch(files: ParsedFile[]): ParsedFile[] {
31
+ const allFilePaths = files.map(f => f.path)
32
+ return files.map(file => ({
33
+ ...file,
34
+ imports: this.resolveAll(file.imports, file.path, allFilePaths)
35
+ }))
36
+ }
37
+
38
+
29
39
  /** Resolve a single import relative to the importing file */
30
40
  resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
31
41
  if (
@@ -43,12 +43,14 @@ export class BM25Index {
43
43
  private documents: BM25Document[] = []
44
44
  private documentFrequency = new Map<string, number>() // term → how many docs contain it
45
45
  private avgDocLength = 0
46
+ private totalDocLength = 0 // running total — avoids O(n²) recompute on every addDocument
46
47
 
47
48
  /** Clear the index */
48
49
  clear(): void {
49
50
  this.documents = []
50
51
  this.documentFrequency.clear()
51
52
  this.avgDocLength = 0
53
+ this.totalDocLength = 0
52
54
  }
53
55
 
54
56
  /** Add a document with pre-tokenized terms */
@@ -62,8 +64,9 @@ export class BM25Index {
62
64
  this.documentFrequency.set(term, (this.documentFrequency.get(term) ?? 0) + 1)
63
65
  }
64
66
 
65
- // Recompute average document length
66
- this.avgDocLength = this.documents.reduce((sum, d) => sum + d.length, 0) / this.documents.length
67
+ // O(1) running average was O(n) reduce over all documents on every insert
68
+ this.totalDocLength += normalizedTokens.length
69
+ this.avgDocLength = this.totalDocLength / this.documents.length
67
70
  }
68
71
 
69
72
  /** Search the index and return ranked results */
@@ -92,7 +95,14 @@ export class BM25Index {
92
95
 
93
96
  // BM25 score component
94
97
  const tfNorm = (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (doc.length / this.avgDocLength)))
95
- score += idf * tfNorm
98
+ let termScore = idf * tfNorm
99
+
100
+ // Bonus for direct name match in the ID
101
+ if (doc.id.toLowerCase().includes(term.toLowerCase())) {
102
+ termScore += 0.5
103
+ }
104
+
105
+ score += termScore
96
106
  }
97
107
 
98
108
  if (score > 0) {
@@ -178,7 +188,9 @@ export function buildFunctionTokens(fn: {
178
188
 
179
189
  // Function name tokens (highest signal)
180
190
  parts.push(...tokenize(fn.name))
181
- parts.push(...tokenize(fn.name)) // Double-weight the name
191
+ parts.push(...tokenize(fn.name))
192
+ parts.push(...tokenize(fn.name)) // Triple-weight the name
193
+ parts.push(`name_exact:${fn.name.toLowerCase()}`)
182
194
 
183
195
  // File path tokens
184
196
  const filename = fn.file.split('/').pop() ?? fn.file
@@ -24,5 +24,5 @@ export function minimatch(filePath: string, pattern: string): boolean {
24
24
  .replace(/\*\*/g, '.*')
25
25
  .replace(/\*/g, '[^/]*')
26
26
 
27
- return new RegExp(`^${regexStr}$`).test(normalizedPath)
27
+ return new RegExp(`^${regexStr}$`, 'i').test(normalizedPath)
28
28
  }
@@ -76,7 +76,7 @@ describe('LockCompiler', () => {
76
76
  const graph = new GraphBuilder().build(files)
77
77
  const lock = compiler.compile(graph, contract, files)
78
78
 
79
- expect(lock.version).toBe('1.7.0')
79
+ expect(lock.version).toBe('2.0.0')
80
80
  expect(lock.syncState.status).toBe('clean')
81
81
  expect(Object.keys(lock.functions).length).toBeGreaterThan(0)
82
82
  expect(Object.keys(lock.modules).length).toBeGreaterThanOrEqual(1)
@@ -89,7 +89,7 @@ describe('LockCompiler', () => {
89
89
  ]
90
90
  const graph = new GraphBuilder().build(files)
91
91
  const lock = compiler.compile(graph, contract, files)
92
- expect(lock.functions['fn:src/auth/verify.ts:verifyToken']?.moduleId).toBe('auth')
92
+ expect(lock.functions['fn:src/auth/verify.ts:verifytoken']?.moduleId).toBe('auth')
93
93
  })
94
94
 
95
95
  it('computes stable module hash', () => {
@@ -31,7 +31,7 @@ function generateDummyLock(graphNodes: Map<string, any>): MikkLock {
31
31
 
32
32
  for (const [id, node] of graphNodes.entries()) {
33
33
  if (node.type === 'function') {
34
- const name = node.label
34
+ const name = node.name
35
35
  const file = node.file
36
36
  // We need to populate `calledBy` for transitive liveness checks
37
37
 
@@ -68,7 +68,7 @@ describe('DeadCodeDetector', () => {
68
68
  // lookups for the lock structure (it only does it for the graph)
69
69
  Object.values(lock.functions).forEach(fn => {
70
70
  const inEdges = graph.inEdges.get(fn.id) ?? []
71
- fn.calledBy = inEdges.filter(e => e.type === 'calls').map(e => e.source)
71
+ fn.calledBy = inEdges.filter(e => e.type === 'calls').map(e => e.from)
72
72
  })
73
73
 
74
74
  const detector = new DeadCodeDetector(graph, lock)
@@ -83,7 +83,7 @@ describe('DeadCodeDetector', () => {
83
83
  const graph = buildTestGraph([
84
84
  ['D', 'nothing']
85
85
  ])
86
- graph.nodes.get('fn:src/D.ts:D')!.metadata!.isExported = true
86
+ graph.nodes.get('fn:src/d.ts:d')!.metadata!.isExported = true
87
87
 
88
88
  const lock = generateDummyLock(graph.nodes)
89
89
 
@@ -115,16 +115,16 @@ describe('DeadCodeDetector', () => {
115
115
  ])
116
116
 
117
117
  // Both in same file by default from buildTestGraph
118
- graph.nodes.get('fn:src/ExportedFn.ts:ExportedFn')!.metadata!.isExported = true
118
+ graph.nodes.get('fn:src/exportedfn.ts:exportedfn')!.metadata!.isExported = true
119
119
 
120
120
  // In the lock, we must place them in the SAME file manually because buildTestGraph
121
121
  // puts them in separate files based on name
122
122
  const lock = generateDummyLock(graph.nodes)
123
- lock.functions['fn:src/ExportedFn.ts:ExportedFn'].file = 'src/shared.ts'
124
- lock.functions['fn:src/InternalHelper.ts:InternalHelper'].file = 'src/shared.ts'
123
+ lock.functions['fn:src/exportedfn.ts:exportedfn'].file = 'src/shared.ts'
124
+ lock.functions['fn:src/internalhelper.ts:internalhelper'].file = 'src/shared.ts'
125
125
 
126
126
  // Set up the calledBy relation
127
- lock.functions['fn:src/InternalHelper.ts:InternalHelper'].calledBy = ['fn:src/ExportedFn.ts:ExportedFn']
127
+ lock.functions['fn:src/internalhelper.ts:internalhelper'].calledBy = ['fn:src/exportedfn.ts:exportedfn']
128
128
 
129
129
  const detector = new DeadCodeDetector(graph, lock)
130
130
  const result = detector.detect()
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
2
+ import * as path from 'node:path'
3
+ import * as fs from 'node:fs/promises'
4
+ import { OxcResolver } from '../src/parser/oxc-resolver'
5
+
6
+ describe('OxcResolver - ESM and CJS Resolution', () => {
7
+ const FIXTURE_DIR = path.join(process.cwd(), '.test-fixture-esm')
8
+
9
+ beforeAll(async () => {
10
+ await fs.mkdir(FIXTURE_DIR, { recursive: true })
11
+
12
+ // 1. Create a "dependency" package with 'exports'
13
+ const depDir = path.join(FIXTURE_DIR, 'node_modules', 'some-pkg')
14
+ await fs.mkdir(depDir, { recursive: true })
15
+
16
+ await fs.writeFile(
17
+ path.join(depDir, 'package.json'),
18
+ JSON.stringify({
19
+ name: 'some-pkg',
20
+ type: 'module',
21
+ exports: {
22
+ '.': {
23
+ import: './dist/esm/index.js',
24
+ require: './dist/cjs/index.cjs',
25
+ types: './dist/index.d.ts'
26
+ },
27
+ './subpath': './dist/sub.js'
28
+ }
29
+ })
30
+ )
31
+
32
+ await fs.mkdir(path.join(depDir, 'dist', 'esm'), { recursive: true })
33
+ await fs.mkdir(path.join(depDir, 'dist', 'cjs'), { recursive: true })
34
+ await fs.writeFile(path.join(depDir, 'dist', 'esm', 'index.js'), 'export const a = 1')
35
+ await fs.writeFile(path.join(depDir, 'dist', 'cjs', 'index.cjs'), 'exports.a = 1')
36
+ await fs.writeFile(path.join(depDir, 'dist', 'sub.js'), 'export const sub = 1')
37
+
38
+ // 2. Create a main package.json
39
+ await fs.writeFile(
40
+ path.join(FIXTURE_DIR, 'package.json'),
41
+ JSON.stringify({
42
+ name: 'main-pkg',
43
+ type: 'module'
44
+ })
45
+ )
46
+ })
47
+
48
+ afterAll(async () => {
49
+ await fs.rm(FIXTURE_DIR, { recursive: true, force: true })
50
+ })
51
+
52
+ it('resolves ESM exports correctly', () => {
53
+ const resolver = new OxcResolver(FIXTURE_DIR)
54
+
55
+ // Resolve 'some-pkg' (should hit exports['.'].import)
56
+ const res = resolver.resolve('some-pkg', path.join(FIXTURE_DIR, 'index.ts'))
57
+ expect(res).toContain('node_modules/some-pkg/dist/esm/index.js')
58
+ })
59
+
60
+ it('resolves subpath exports correctly', () => {
61
+ const resolver = new OxcResolver(FIXTURE_DIR)
62
+
63
+ // Resolve 'some-pkg/subpath'
64
+ const res = resolver.resolve('some-pkg/subpath', path.join(FIXTURE_DIR, 'index.ts'))
65
+ expect(res).toContain('node_modules/some-pkg/dist/sub.js')
66
+ })
67
+
68
+ it('resolves relative imports with extension probing', async () => {
69
+ const resolver = new OxcResolver(FIXTURE_DIR)
70
+ await fs.writeFile(path.join(FIXTURE_DIR, 'local.ts'), 'export const x = 1')
71
+
72
+ const res = resolver.resolve('./local', path.join(FIXTURE_DIR, 'index.ts'))
73
+ expect(res).toContain('.test-fixture-esm/local.ts')
74
+ })
75
+ })
@@ -19,8 +19,8 @@ describe('GraphBuilder', () => {
19
19
  mockParsedFile('src/auth.ts', [mockFunction('verifyToken', [], 'src/auth.ts')]),
20
20
  ]
21
21
  const graph = builder.build(files)
22
- expect(graph.nodes.has('fn:src/auth.ts:verifyToken')).toBe(true)
23
- expect(graph.nodes.get('fn:src/auth.ts:verifyToken')!.type).toBe('function')
22
+ expect(graph.nodes.has('fn:src/auth.ts:verifytoken')).toBe(true)
23
+ expect(graph.nodes.get('fn:src/auth.ts:verifytoken')!.type).toBe('function')
24
24
  })
25
25
 
26
26
  it('creates edges for imports', () => {
@@ -35,8 +35,8 @@ describe('GraphBuilder', () => {
35
35
  const graph = builder.build(files)
36
36
  const importEdges = graph.edges.filter(e => e.type === 'imports')
37
37
  expect(importEdges.length).toBeGreaterThanOrEqual(1)
38
- expect(importEdges[0].source).toBe('src/auth.ts')
39
- expect(importEdges[0].target).toBe('src/utils/jwt.ts')
38
+ expect(importEdges[0].from).toBe('src/auth.ts')
39
+ expect(importEdges[0].to).toBe('src/utils/jwt.ts')
40
40
  })
41
41
 
42
42
  it('creates edges for function calls via imports', () => {
@@ -51,8 +51,8 @@ describe('GraphBuilder', () => {
51
51
  const graph = builder.build(files)
52
52
  const callEdges = graph.edges.filter(e => e.type === 'calls')
53
53
  expect(callEdges.length).toBeGreaterThanOrEqual(1)
54
- expect(callEdges[0].source).toBe('fn:src/auth.ts:verifyToken')
55
- expect(callEdges[0].target).toBe('fn:src/utils/jwt.ts:jwtDecode')
54
+ expect(callEdges[0].from).toBe('fn:src/auth.ts:verifytoken')
55
+ expect(callEdges[0].to).toBe('fn:src/utils/jwt.ts:jwtdecode')
56
56
  })
57
57
 
58
58
  it('creates containment edges', () => {
@@ -62,8 +62,8 @@ describe('GraphBuilder', () => {
62
62
  const graph = builder.build(files)
63
63
  const containEdges = graph.edges.filter(e => e.type === 'contains')
64
64
  expect(containEdges.length).toBeGreaterThanOrEqual(1)
65
- expect(containEdges[0].source).toBe('src/auth.ts')
66
- expect(containEdges[0].target).toBe('fn:src/auth.ts:verifyToken')
65
+ expect(containEdges[0].from).toBe('src/auth.ts')
66
+ expect(containEdges[0].to).toBe('fn:src/auth.ts:verifytoken')
67
67
  })
68
68
 
69
69
  it('builds adjacency maps', () => {
@@ -72,7 +72,7 @@ describe('GraphBuilder', () => {
72
72
  ]
73
73
  const graph = builder.build(files)
74
74
  expect(graph.outEdges.has('src/auth.ts')).toBe(true)
75
- expect(graph.inEdges.has('fn:src/auth.ts:verifyToken')).toBe(true)
75
+ expect(graph.inEdges.has('fn:src/auth.ts:verifytoken')).toBe(true)
76
76
  })
77
77
  })
78
78
 
@@ -83,8 +83,8 @@ describe('ImpactAnalyzer', () => {
83
83
  ['B', 'nothing'],
84
84
  ])
85
85
  const analyzer = new ImpactAnalyzer(graph)
86
- const result = analyzer.analyze(['fn:src/B.ts:B'])
87
- expect(result.impacted).toContain('fn:src/A.ts:A')
86
+ const result = analyzer.analyze(['fn:src/b.ts:b'])
87
+ expect(result.impacted).toContain('fn:src/a.ts:a')
88
88
  })
89
89
 
90
90
  it('finds transitive dependents', () => {
@@ -94,9 +94,9 @@ describe('ImpactAnalyzer', () => {
94
94
  ['C', 'nothing'],
95
95
  ])
96
96
  const analyzer = new ImpactAnalyzer(graph)
97
- const result = analyzer.analyze(['fn:src/C.ts:C'])
98
- expect(result.impacted).toContain('fn:src/B.ts:B')
99
- expect(result.impacted).toContain('fn:src/A.ts:A')
97
+ const result = analyzer.analyze(['fn:src/c.ts:c'])
98
+ expect(result.impacted).toContain('fn:src/b.ts:b')
99
+ expect(result.impacted).toContain('fn:src/a.ts:a')
100
100
  })
101
101
 
102
102
  it('reports correct depth', () => {
@@ -107,7 +107,7 @@ describe('ImpactAnalyzer', () => {
107
107
  ['D', 'nothing'],
108
108
  ])
109
109
  const analyzer = new ImpactAnalyzer(graph)
110
- const result = analyzer.analyze(['fn:src/D.ts:D'])
110
+ const result = analyzer.analyze(['fn:src/d.ts:d'])
111
111
  expect(result.depth).toBeGreaterThanOrEqual(3)
112
112
  })
113
113
 
@@ -117,8 +117,8 @@ describe('ImpactAnalyzer', () => {
117
117
  ['B', 'nothing'],
118
118
  ])
119
119
  const analyzer = new ImpactAnalyzer(graph)
120
- const result = analyzer.analyze(['fn:src/B.ts:B'])
121
- expect(result.confidence).toBe('high')
120
+ const result = analyzer.analyze(['fn:src/b.ts:b'])
121
+ expect(result.confidence).toBeGreaterThanOrEqual(0.8)
122
122
  })
123
123
 
124
124
  it('does not include changed nodes in impacted', () => {
@@ -127,9 +127,9 @@ describe('ImpactAnalyzer', () => {
127
127
  ['B', 'nothing'],
128
128
  ])
129
129
  const analyzer = new ImpactAnalyzer(graph)
130
- const result = analyzer.analyze(['fn:src/B.ts:B'])
131
- expect(result.impacted).not.toContain('fn:src/B.ts:B')
132
- expect(result.changed).toContain('fn:src/B.ts:B')
130
+ const result = analyzer.analyze(['fn:src/b.ts:b'])
131
+ expect(result.impacted).not.toContain('fn:src/b.ts:b')
132
+ expect(result.changed).toContain('fn:src/b.ts:b')
133
133
  })
134
134
  })
135
135
 
package/tests/helpers.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { hashContent } from '../src/hash/file-hasher'
2
- import type { ParsedFile, ParsedFunction, ParsedImport } from '../src/parser/types'
2
+ import type { ParsedFile, ParsedFunction, ParsedImport, CallExpression } from '../src/parser/types'
3
3
  import { GraphBuilder } from '../src/graph/graph-builder'
4
4
  import type { DependencyGraph } from '../src/graph/types'
5
5
 
@@ -10,7 +10,7 @@ export function mockParsedFile(
10
10
  imports: ParsedImport[] = []
11
11
  ): ParsedFile {
12
12
  return {
13
- path: filePath,
13
+ path: filePath.replace(/\\/g, '/'),
14
14
  language: 'typescript',
15
15
  functions,
16
16
  classes: [],
@@ -26,25 +26,30 @@ export function mockParsedFile(
26
26
  /** Build a minimal ParsedFunction */
27
27
  export function mockFunction(
28
28
  name: string,
29
- calls: string[] = [],
29
+ calls: (string | CallExpression)[] = [],
30
30
  file: string = 'src/test.ts',
31
31
  isExported: boolean = false
32
32
  ): ParsedFunction {
33
+ const mappedCalls: CallExpression[] = calls.map(c =>
34
+ typeof c === 'string' ? { name: c, line: 1, type: 'function' } : c
35
+ )
36
+ const normalizedPath = file.replace(/\\/g, '/')
33
37
  return {
34
- id: `fn:${file}:${name}`,
38
+ id: `fn:${normalizedPath}:${name}`.toLowerCase(),
35
39
  name,
36
- file,
40
+ file: normalizedPath,
37
41
  startLine: 1,
38
42
  endLine: 10,
39
43
  params: [],
40
44
  returnType: 'void',
41
45
  isExported,
42
46
  isAsync: false,
43
- calls,
47
+ calls: mappedCalls,
44
48
  hash: hashContent(name),
45
49
  purpose: '',
46
50
  edgeCasesHandled: [],
47
51
  errorHandling: [],
52
+ detailedLines: [],
48
53
  }
49
54
  }
50
55