@getmikk/core 2.0.13 → 2.0.15

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 (71) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/src/analysis/index.ts +9 -0
  4. package/src/analysis/taint-analysis.ts +419 -0
  5. package/src/analysis/type-flow.ts +247 -0
  6. package/src/cache/incremental-cache.ts +278 -0
  7. package/src/cache/index.ts +1 -0
  8. package/src/contract/contract-generator.ts +31 -3
  9. package/src/contract/contract-reader.ts +1 -0
  10. package/src/contract/lock-compiler.ts +125 -12
  11. package/src/contract/schema.ts +4 -0
  12. package/src/error-handler.ts +2 -1
  13. package/src/graph/cluster-detector.ts +2 -4
  14. package/src/graph/dead-code-detector.ts +303 -117
  15. package/src/graph/graph-builder.ts +21 -161
  16. package/src/graph/impact-analyzer.ts +1 -0
  17. package/src/graph/index.ts +2 -0
  18. package/src/graph/rich-function-index.ts +1080 -0
  19. package/src/graph/symbol-table.ts +252 -0
  20. package/src/hash/hash-store.ts +1 -0
  21. package/src/index.ts +4 -0
  22. package/src/parser/base-extractor.ts +19 -0
  23. package/src/parser/boundary-checker.ts +31 -12
  24. package/src/parser/error-recovery.ts +647 -0
  25. package/src/parser/function-body-extractor.ts +248 -0
  26. package/src/parser/go/go-extractor.ts +249 -676
  27. package/src/parser/index.ts +138 -295
  28. package/src/parser/language-registry.ts +57 -0
  29. package/src/parser/oxc-parser.ts +166 -28
  30. package/src/parser/oxc-resolver.ts +179 -11
  31. package/src/parser/parser-constants.ts +1 -0
  32. package/src/parser/rust/rust-extractor.ts +109 -0
  33. package/src/parser/tree-sitter/parser.ts +400 -66
  34. package/src/parser/tree-sitter/queries.ts +106 -10
  35. package/src/parser/types.ts +20 -1
  36. package/src/search/bm25.ts +21 -8
  37. package/src/search/direct-search.ts +472 -0
  38. package/src/search/embedding-provider.ts +249 -0
  39. package/src/search/index.ts +12 -0
  40. package/src/search/semantic-search.ts +435 -0
  41. package/src/security/index.ts +1 -0
  42. package/src/security/scanner.ts +342 -0
  43. package/src/utils/artifact-transaction.ts +1 -0
  44. package/src/utils/atomic-write.ts +1 -0
  45. package/src/utils/errors.ts +89 -4
  46. package/src/utils/fs.ts +150 -65
  47. package/src/utils/json.ts +1 -0
  48. package/src/utils/language-registry.ts +96 -5
  49. package/src/utils/minimatch.ts +49 -6
  50. package/src/utils/path.ts +26 -0
  51. package/tests/dead-code.test.ts +3 -2
  52. package/tests/direct-search.test.ts +435 -0
  53. package/tests/error-recovery.test.ts +143 -0
  54. package/tests/fixtures/simple-api/src/index.ts +1 -1
  55. package/tests/go-parser.test.ts +19 -335
  56. package/tests/js-parser.test.ts +18 -1089
  57. package/tests/language-registry-all.test.ts +276 -0
  58. package/tests/language-registry.test.ts +6 -4
  59. package/tests/parse-diagnostics.test.ts +9 -96
  60. package/tests/parser.test.ts +42 -771
  61. package/tests/polyglot-parser.test.ts +117 -0
  62. package/tests/rich-function-index.test.ts +703 -0
  63. package/tests/tree-sitter-parser.test.ts +108 -80
  64. package/tests/ts-parser.test.ts +8 -8
  65. package/tests/verification.test.ts +175 -0
  66. package/src/parser/base-parser.ts +0 -16
  67. package/src/parser/go/go-parser.ts +0 -43
  68. package/src/parser/javascript/js-extractor.ts +0 -278
  69. package/src/parser/javascript/js-parser.ts +0 -101
  70. package/src/parser/typescript/ts-extractor.ts +0 -447
  71. package/src/parser/typescript/ts-parser.ts +0 -36
@@ -0,0 +1,1080 @@
1
+ import type { DependencyGraph } from './types.js'
2
+ import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
3
+
4
+ export interface RichFunction {
5
+ id: string
6
+ name: string
7
+ file: string
8
+ moduleId: string
9
+ startLine: number
10
+ endLine: number
11
+ params: RichParam[]
12
+ returnType: string
13
+ isExported: boolean
14
+ isAsync: boolean
15
+ isGenerator: boolean
16
+ typeParameters: string[]
17
+ body: string
18
+ purpose: string
19
+ docComment: string
20
+ decorators: string[]
21
+ calls: RichCall[]
22
+ calledBy: string[]
23
+ edgeCasesHandled: string[]
24
+ errorHandling: RichErrorHandling[]
25
+ complexity: number
26
+ cyclomaticComplexity: number
27
+ cognitiveComplexity: number
28
+ dependencies: string[]
29
+ affectedBy: string[]
30
+ keywords: string[]
31
+ signature: string
32
+ fullSignature: string
33
+ contentHash?: string
34
+ signatureHash?: string
35
+ paramHashes?: string[]
36
+ }
37
+
38
+ export interface RichParam {
39
+ name: string
40
+ type: string
41
+ optional: boolean
42
+ defaultValue?: string
43
+ destructured?: boolean
44
+ rest?: boolean
45
+ }
46
+
47
+ export interface RichCall {
48
+ name: string
49
+ line: number
50
+ column: number
51
+ type: 'function' | 'method' | 'property' | 'new' | 'await' | 'yield'
52
+ targetId?: string
53
+ arguments: string[]
54
+ }
55
+
56
+ export interface RichErrorHandling {
57
+ line: number
58
+ endLine: number
59
+ type: 'try-catch' | 'throw' | 'return-error' | 'if-error'
60
+ detail: string
61
+ caughtTypes: string[]
62
+ handled: boolean
63
+ }
64
+
65
+ export interface SearchQuery {
66
+ text?: string
67
+ name?: string
68
+ file?: string
69
+ moduleId?: string
70
+ exactName?: string
71
+ nameContains?: string
72
+ nameStartsWith?: string
73
+ namePattern?: RegExp
74
+ returnType?: string
75
+ returnTypeContains?: string
76
+ paramTypes?: string[]
77
+ hasParam?: string
78
+ hasDecorator?: string
79
+ isExported?: boolean
80
+ isAsync?: boolean
81
+ isGenerator?: boolean
82
+ minParams?: number
83
+ maxParams?: number
84
+ keyword?: string
85
+ calls?: string
86
+ calledBy?: string
87
+ inFile?: string
88
+ inModule?: string
89
+ exportedFrom?: string
90
+ limit?: number
91
+ offset?: number
92
+ }
93
+
94
+ export interface SearchResult {
95
+ function: RichFunction
96
+ score: number
97
+ matchReasons: string[]
98
+ }
99
+
100
+ export interface ContextRequest {
101
+ functionId: string
102
+ include?: 'signature' | 'full' | 'body' | 'calls' | 'calledBy' | 'all'
103
+ maxBodyLines?: number
104
+ }
105
+
106
+ export interface FunctionContext {
107
+ signature: string
108
+ fullSignature: string
109
+ body?: string
110
+ purpose?: string
111
+ docComment?: string
112
+ params: RichParam[]
113
+ returnType: string
114
+ calls: RichCall[]
115
+ calledBy: string[]
116
+ decorators: string[]
117
+ file: string
118
+ startLine: number
119
+ endLine: number
120
+ errorHandling?: RichErrorHandling[]
121
+ edgeCases?: string[]
122
+ keywords: string[]
123
+ }
124
+
125
+ export class RichFunctionIndex {
126
+ private functions: Map<string, RichFunction> = new Map()
127
+ private byName: Map<string, string[]> = new Map()
128
+ private byFile: Map<string, string[]> = new Map()
129
+ private byModule: Map<string, string[]> = new Map()
130
+ private byExport: Map<boolean, string[]> = new Map()
131
+ private byReturnType: Map<string, string[]> = new Map()
132
+ private byParamType: Map<string, Set<string>> = new Map()
133
+ private byDecorator: Map<string, string[]> = new Map()
134
+ private byKeyword: Map<string, Set<string>> = new Map()
135
+ private byCall: Map<string, Set<string>> = new Map()
136
+ private byCalledBy: Map<string, Set<string>> = new Map()
137
+ private nameIndex: Map<string, string> = new Map()
138
+ private textIndex: Map<string, Set<string>> = new Map()
139
+ private allKeywords: Set<string> = new Set()
140
+
141
+ private bySignatureHash: Map<string, string> = new Map()
142
+ private byContentHash: Map<string, string> = new Map()
143
+ private byParamHash: Map<string, string[]> = new Map()
144
+
145
+ constructor() {}
146
+
147
+ index(lock: MikkLock, graph?: DependencyGraph): void {
148
+ this.clear()
149
+
150
+ for (const [id, fn] of Object.entries(lock.functions)) {
151
+ const rich = this.enrichFunction(fn, lock, graph)
152
+ this.addFunction(rich)
153
+ }
154
+ }
155
+
156
+ private simpleHash(str: string): string {
157
+ let hash = 0
158
+ for (let i = 0; i < str.length; i++) {
159
+ const char = str.charCodeAt(i)
160
+ hash = ((hash << 5) - hash) + char
161
+ hash = hash & hash
162
+ }
163
+ return Math.abs(hash).toString(36)
164
+ }
165
+
166
+ private enrichFunction(fn: MikkLockFunction, lock: MikkLock, graph?: DependencyGraph): RichFunction {
167
+ const id = fn.id
168
+ const name = fn.name || this.parseNameFromId(id)
169
+ const file = fn.file || this.parseFileFromId(id)
170
+
171
+ const calls = this.extractCalls(fn, lock)
172
+ const calledBy = this.extractCalledBy(fn, lock)
173
+ const keywords = this.extractKeywords(fn, calls)
174
+ const signature = this.buildSignature(fn)
175
+ const fullSignature = this.buildFullSignature(fn)
176
+
177
+ const richParams: RichParam[] = (fn.params || []).map(p => ({
178
+ name: p.name,
179
+ type: p.type,
180
+ optional: p.optional ?? false,
181
+ defaultValue: undefined,
182
+ destructured: false,
183
+ rest: false,
184
+ }))
185
+
186
+ const richErrors: RichErrorHandling[] = (fn.errorHandling || []).map(e => ({
187
+ line: e.line,
188
+ endLine: e.line,
189
+ type: e.type,
190
+ detail: e.detail,
191
+ caughtTypes: [],
192
+ handled: true,
193
+ }))
194
+
195
+ const signatureHash = this.simpleHash(fullSignature)
196
+ const contentHash = fn.hash || this.simpleHash(`${file}:${name}:${signatureHash}`)
197
+ const paramHashes = richParams.map(p => this.simpleHash(`${p.name}:${p.type}`))
198
+
199
+ return {
200
+ id,
201
+ name,
202
+ file,
203
+ moduleId: fn.moduleId || 'unknown',
204
+ startLine: fn.startLine || 0,
205
+ endLine: fn.endLine || 0,
206
+ params: richParams,
207
+ returnType: fn.returnType || 'void',
208
+ isExported: fn.isExported || false,
209
+ isAsync: fn.isAsync || false,
210
+ isGenerator: false,
211
+ typeParameters: [],
212
+ body: '',
213
+ purpose: fn.purpose || this.inferPurpose(name, fn),
214
+ docComment: '',
215
+ decorators: [],
216
+ calls,
217
+ calledBy,
218
+ edgeCasesHandled: fn.edgeCasesHandled || [],
219
+ errorHandling: richErrors,
220
+ complexity: this.calculateComplexity(fn),
221
+ cyclomaticComplexity: this.calculateCyclomaticComplexity(fn),
222
+ cognitiveComplexity: 0,
223
+ dependencies: calls.map(c => c.name),
224
+ affectedBy: calledBy,
225
+ keywords,
226
+ signature,
227
+ fullSignature,
228
+ contentHash,
229
+ signatureHash,
230
+ paramHashes,
231
+ }
232
+ }
233
+
234
+ private extractCalls(fn: MikkLockFunction, lock: MikkLock): RichCall[] {
235
+ const calls: RichCall[] = []
236
+
237
+ for (const calleeId of fn.calls || []) {
238
+ const callee = lock.functions[calleeId]
239
+ if (callee) {
240
+ calls.push({
241
+ name: callee.name || this.parseNameFromId(calleeId),
242
+ line: 0,
243
+ column: 0,
244
+ type: 'function',
245
+ targetId: calleeId,
246
+ arguments: [],
247
+ })
248
+ }
249
+ }
250
+
251
+ return calls
252
+ }
253
+
254
+ private extractCalledBy(fn: MikkLockFunction, lock: MikkLock): string[] {
255
+ const calledBy: string[] = []
256
+
257
+ for (const callerId of fn.calledBy || []) {
258
+ const caller = lock.functions[callerId]
259
+ if (caller) {
260
+ calledBy.push(callerId)
261
+ }
262
+ }
263
+
264
+ return calledBy
265
+ }
266
+
267
+ private extractKeywords(fn: MikkLockFunction, calls: RichCall[]): string[] {
268
+ const keywords = new Set<string>()
269
+
270
+ const name = fn.name || ''
271
+ const words = name.match(/[A-Z][a-z]+|[a-z]+/g) || []
272
+ words.forEach(w => keywords.add(w.toLowerCase()))
273
+
274
+ if (fn.purpose) {
275
+ const purposeWords = fn.purpose.match(/[a-z]{3,}/g) || []
276
+ purposeWords.forEach(w => keywords.add(w.toLowerCase()))
277
+ }
278
+
279
+ const returnType = fn.returnType || ''
280
+ if (returnType.includes('Promise')) keywords.add('async')
281
+ if (returnType.includes('Error') || returnType.includes('Result')) keywords.add('error-handling')
282
+ if (returnType !== 'void' && returnType !== 'never') keywords.add('returns-value')
283
+
284
+ if (fn.isAsync) keywords.add('async')
285
+
286
+ for (const call of calls) {
287
+ keywords.add(call.name.toLowerCase())
288
+ }
289
+
290
+ return [...keywords]
291
+ }
292
+
293
+ private buildSignature(fn: MikkLockFunction): string {
294
+ const params = (fn.params || [])
295
+ .map(p => `${p.name}${p.optional ? '?' : ''}: ${p.type}`)
296
+ .join(', ')
297
+ return `${fn.name || 'anonymous'}(${params})`
298
+ }
299
+
300
+ private buildFullSignature(fn: MikkLockFunction): string {
301
+ const asyncPrefix = fn.isAsync ? 'async ' : ''
302
+ const params = (fn.params || [])
303
+ .map(p => {
304
+ let str = p.name
305
+ if (p.optional) str += '?'
306
+ str += `: ${p.type}`
307
+ return str
308
+ })
309
+ .join(', ')
310
+ const returnType = fn.returnType || 'void'
311
+ return `${asyncPrefix}${fn.name || 'anonymous'}(${params}): ${returnType}`
312
+ }
313
+
314
+ private parseNameFromId(id: string): string {
315
+ const parts = id.split(':')
316
+ return parts[parts.length - 1] || 'anonymous'
317
+ }
318
+
319
+ private parseFileFromId(id: string): string {
320
+ const withoutPrefix = id.replace(/^(fn|class|type|intf|enum):/, '')
321
+ const parts = withoutPrefix.split(':')
322
+ return parts.slice(0, -1).join(':')
323
+ }
324
+
325
+ private inferPurpose(name: string, fn: MikkLockFunction): string {
326
+ const lower = name.toLowerCase()
327
+
328
+ if (lower.startsWith('get') || lower.startsWith('fetch') || lower.startsWith('load')) {
329
+ return `Retrieves data`
330
+ }
331
+ if (lower.startsWith('set') || lower.startsWith('update') || lower.startsWith('save')) {
332
+ return `Modifies or persists data`
333
+ }
334
+ if (lower.startsWith('create') || lower.startsWith('add') || lower.startsWith('new')) {
335
+ return `Creates a new entity`
336
+ }
337
+ if (lower.startsWith('delete') || lower.startsWith('remove') || lower.startsWith('destroy')) {
338
+ return `Removes an entity`
339
+ }
340
+ if (lower.startsWith('is') || lower.startsWith('has') || lower.startsWith('can')) {
341
+ return `Checks a condition`
342
+ }
343
+ if (lower.startsWith('validate') || lower.startsWith('check')) {
344
+ return `Validates input`
345
+ }
346
+ if (lower.startsWith('parse') || lower.startsWith('transform') || lower.startsWith('convert')) {
347
+ return `Transforms data format`
348
+ }
349
+ if (lower.startsWith('handle') || lower.startsWith('process')) {
350
+ return `Handles processing logic`
351
+ }
352
+ if (lower.startsWith('render') || lower.startsWith('display')) {
353
+ return `Renders output`
354
+ }
355
+ if (lower.startsWith('init') || lower.startsWith('setup') || lower.startsWith('configure')) {
356
+ return `Initializes configuration`
357
+ }
358
+
359
+ return ''
360
+ }
361
+
362
+ private calculateComplexity(fn: MikkLockFunction): number {
363
+ let score = 1
364
+ if (fn.params && fn.params.length > 3) score += 1
365
+ if (fn.isAsync) score += 1
366
+ if ((fn.calls || []).length > 10) score += Math.floor((fn.calls || []).length / 10)
367
+ return score
368
+ }
369
+
370
+ private calculateCyclomaticComplexity(fn: MikkLockFunction): number {
371
+ let complexity = 1
372
+ if ((fn.errorHandling || []).length > 0) {
373
+ complexity += (fn.errorHandling || []).filter(e => e.type === 'try-catch').length
374
+ }
375
+ return complexity
376
+ }
377
+
378
+ private addFunction(rich: RichFunction): void {
379
+ this.functions.set(rich.id, rich)
380
+
381
+ const nameLower = rich.name.toLowerCase()
382
+
383
+ const nameSet = this.byName.get(nameLower) || []
384
+ nameSet.push(rich.id)
385
+ this.byName.set(nameLower, nameSet)
386
+
387
+ const fileSet = this.byFile.get(rich.file) || []
388
+ fileSet.push(rich.id)
389
+ this.byFile.set(rich.file, fileSet)
390
+
391
+ const moduleSet = this.byModule.get(rich.moduleId) || []
392
+ moduleSet.push(rich.id)
393
+ this.byModule.set(rich.moduleId, moduleSet)
394
+
395
+ const exportedSet = this.byExport.get(rich.isExported) || []
396
+ exportedSet.push(rich.id)
397
+ this.byExport.set(rich.isExported, exportedSet)
398
+
399
+ const returnTypeLower = rich.returnType.toLowerCase()
400
+ const returnSet = this.byReturnType.get(returnTypeLower) || []
401
+ returnSet.push(rich.id)
402
+ this.byReturnType.set(returnTypeLower, returnSet)
403
+
404
+ for (const param of rich.params) {
405
+ const typeLower = param.type.toLowerCase()
406
+ if (!this.byParamType.has(typeLower)) {
407
+ this.byParamType.set(typeLower, new Set())
408
+ }
409
+ this.byParamType.get(typeLower)!.add(rich.id)
410
+ }
411
+
412
+ for (const decorator of rich.decorators) {
413
+ const decLower = decorator.toLowerCase()
414
+ const decSet = this.byDecorator.get(decLower) || []
415
+ decSet.push(rich.id)
416
+ this.byDecorator.set(decLower, decSet)
417
+ }
418
+
419
+ for (const keyword of rich.keywords) {
420
+ if (!this.byKeyword.has(keyword)) {
421
+ this.byKeyword.set(keyword, new Set())
422
+ }
423
+ this.byKeyword.get(keyword)!.add(rich.id)
424
+ this.allKeywords.add(keyword)
425
+ }
426
+
427
+ for (const call of rich.calls) {
428
+ if (!this.byCall.has(call.name)) {
429
+ this.byCall.set(call.name, new Set())
430
+ }
431
+ this.byCall.get(call.name)!.add(rich.id)
432
+ }
433
+
434
+ for (const callerId of rich.calledBy) {
435
+ if (!this.byCalledBy.has(callerId)) {
436
+ this.byCalledBy.set(callerId, new Set())
437
+ }
438
+ this.byCalledBy.get(callerId)!.add(rich.id)
439
+ }
440
+
441
+ this.nameIndex.set(rich.name, rich.id)
442
+
443
+ if (rich.signatureHash) {
444
+ this.bySignatureHash.set(rich.signatureHash, rich.id)
445
+ }
446
+ if (rich.contentHash) {
447
+ this.byContentHash.set(rich.contentHash, rich.id)
448
+ }
449
+ for (const paramHash of rich.paramHashes || []) {
450
+ const existing = this.byParamHash.get(paramHash) || []
451
+ existing.push(rich.id)
452
+ this.byParamHash.set(paramHash, existing)
453
+ }
454
+
455
+ const fullText = [
456
+ rich.name,
457
+ rich.file,
458
+ rich.purpose,
459
+ rich.returnType,
460
+ ...rich.params.map(p => p.name + ' ' + p.type),
461
+ ...rich.keywords,
462
+ ].join(' ').toLowerCase()
463
+
464
+ const tokens = fullText.split(/\s+/)
465
+ for (const token of tokens) {
466
+ if (token.length >= 2) {
467
+ if (!this.textIndex.has(token)) {
468
+ this.textIndex.set(token, new Set())
469
+ }
470
+ this.textIndex.get(token)!.add(rich.id)
471
+ }
472
+ }
473
+ }
474
+
475
+ private clear(): void {
476
+ this.functions.clear()
477
+ this.byName.clear()
478
+ this.byFile.clear()
479
+ this.byModule.clear()
480
+ this.byExport.clear()
481
+ this.byReturnType.clear()
482
+ this.byParamType.clear()
483
+ this.byDecorator.clear()
484
+ this.byKeyword.clear()
485
+ this.byCall.clear()
486
+ this.byCalledBy.clear()
487
+ this.nameIndex.clear()
488
+ this.textIndex.clear()
489
+ this.allKeywords.clear()
490
+ this.bySignatureHash.clear()
491
+ this.byContentHash.clear()
492
+ this.byParamHash.clear()
493
+ }
494
+
495
+ getBySignatureHash(hash: string): RichFunction | undefined {
496
+ const id = this.bySignatureHash.get(hash)
497
+ return id ? this.functions.get(id) : undefined
498
+ }
499
+
500
+ getByContentHash(hash: string): RichFunction | undefined {
501
+ const id = this.byContentHash.get(hash)
502
+ return id ? this.functions.get(id) : undefined
503
+ }
504
+
505
+ findBySignature(signature: string): RichFunction | undefined {
506
+ const hash = this.simpleHash(signature)
507
+ return this.getBySignatureHash(hash)
508
+ }
509
+
510
+ findByParamTypes(paramTypes: string[]): RichFunction[] {
511
+ if (paramTypes.length === 0) return []
512
+
513
+ const paramHashes = paramTypes.map(pt => this.simpleHash(pt))
514
+ const candidates = this.byParamHash.get(paramHashes[0]) || []
515
+
516
+ if (paramTypes.length === 1) {
517
+ return candidates.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
518
+ }
519
+
520
+ return candidates
521
+ .map(id => this.functions.get(id))
522
+ .filter((fn): fn is RichFunction => {
523
+ if (!fn) return false
524
+ const fnParamHashes = fn.paramHashes || []
525
+ return paramHashes.every(ph => fnParamHashes.includes(ph))
526
+ })
527
+ }
528
+
529
+ findByLocation(file: string, line: number): RichFunction | undefined {
530
+ const fnsInFile = this.byFile.get(file)
531
+ if (!fnsInFile) return undefined
532
+
533
+ for (const id of fnsInFile) {
534
+ const fn = this.functions.get(id)
535
+ if (fn && line >= fn.startLine && line <= fn.endLine) {
536
+ return fn
537
+ }
538
+ }
539
+ return undefined
540
+ }
541
+
542
+ findBySignatureAndParams(signature: string, paramTypes?: string[]): RichFunction | undefined {
543
+ const fn = this.findBySignature(signature)
544
+ if (!fn) return undefined
545
+
546
+ if (paramTypes && paramTypes.length > 0) {
547
+ const fnParamTypes = fn.params.map(p => p.type)
548
+ const matches = paramTypes.every(pt => fnParamTypes.some(fpt => fpt.includes(pt)))
549
+ if (!matches) return undefined
550
+ }
551
+
552
+ return fn
553
+ }
554
+
555
+ get(id: string): RichFunction | undefined {
556
+ return this.functions.get(id)
557
+ }
558
+
559
+ getByName(name: string): RichFunction | undefined {
560
+ const id = this.nameIndex.get(name)
561
+ return id ? this.functions.get(id) : undefined
562
+ }
563
+
564
+ getByExactName(name: string): RichFunction | undefined {
565
+ const ids = this.byName.get(name.toLowerCase())
566
+ if (ids && ids.length > 0) {
567
+ return this.functions.get(ids[0])
568
+ }
569
+ return undefined
570
+ }
571
+
572
+ getByFile(file: string): RichFunction[] {
573
+ const ids = this.byFile.get(file) || []
574
+ return ids.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
575
+ }
576
+
577
+ getByModule(moduleId: string): RichFunction[] {
578
+ const ids = this.byModule.get(moduleId) || []
579
+ return ids.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
580
+ }
581
+
582
+ getExported(): RichFunction[] {
583
+ const ids = this.byExport.get(true) || []
584
+ return ids.map(id => this.functions.get(id)).filter(Boolean) as RichFunction[]
585
+ }
586
+
587
+ getAll(): RichFunction[] {
588
+ return [...this.functions.values()]
589
+ }
590
+
591
+ getCount(): number {
592
+ return this.functions.size
593
+ }
594
+
595
+ search(query: SearchQuery): SearchResult[] {
596
+ let candidateIds: Set<string> | null = null
597
+ const matchReasons: string[] = []
598
+
599
+ if (query.name) {
600
+ const ids = this.byName.get(query.name.toLowerCase())
601
+ if (ids) {
602
+ candidateIds = new Set(ids)
603
+ matchReasons.push(`name match: ${query.name}`)
604
+ }
605
+ }
606
+
607
+ if (query.exactName) {
608
+ const fn = this.getByExactName(query.exactName)
609
+ if (fn) {
610
+ candidateIds = new Set([fn.id])
611
+ matchReasons.push(`exact name: ${query.exactName}`)
612
+ } else {
613
+ return []
614
+ }
615
+ }
616
+
617
+ if (query.nameContains) {
618
+ const lower = query.nameContains.toLowerCase()
619
+ const matching = [...this.byName.entries()]
620
+ .filter(([name]) => name.includes(lower))
621
+ .flatMap(([, ids]) => ids)
622
+
623
+ if (candidateIds) {
624
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
625
+ } else {
626
+ candidateIds = new Set(matching)
627
+ }
628
+ matchReasons.push(`name contains: ${query.nameContains}`)
629
+ }
630
+
631
+ if (query.nameStartsWith) {
632
+ const lower = query.nameStartsWith.toLowerCase()
633
+ const matching = [...this.byName.entries()]
634
+ .filter(([name]) => name.startsWith(lower))
635
+ .flatMap(([, ids]) => ids)
636
+
637
+ if (candidateIds) {
638
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
639
+ } else {
640
+ candidateIds = new Set(matching)
641
+ }
642
+ matchReasons.push(`name starts with: ${query.nameStartsWith}`)
643
+ }
644
+
645
+ if (query.file) {
646
+ const ids = this.byFile.get(query.file) || []
647
+ if (candidateIds) {
648
+ candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
649
+ } else {
650
+ candidateIds = new Set(ids)
651
+ }
652
+ matchReasons.push(`file: ${query.file}`)
653
+ }
654
+
655
+ if (query.inFile) {
656
+ const lower = query.inFile.toLowerCase()
657
+ const matching = [...this.byFile.entries()]
658
+ .filter(([file]) => file.toLowerCase().includes(lower))
659
+ .flatMap(([, ids]) => ids)
660
+
661
+ if (candidateIds) {
662
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
663
+ } else {
664
+ candidateIds = new Set(matching)
665
+ }
666
+ matchReasons.push(`in file containing: ${query.inFile}`)
667
+ }
668
+
669
+ if (query.moduleId) {
670
+ const ids = this.byModule.get(query.moduleId) || []
671
+ if (candidateIds) {
672
+ candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
673
+ } else {
674
+ candidateIds = new Set(ids)
675
+ }
676
+ matchReasons.push(`module: ${query.moduleId}`)
677
+ }
678
+
679
+ if (query.isExported !== undefined) {
680
+ const ids = this.byExport.get(query.isExported) || []
681
+ if (candidateIds) {
682
+ candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
683
+ } else {
684
+ candidateIds = new Set(ids)
685
+ }
686
+ matchReasons.push(`isExported: ${query.isExported}`)
687
+ }
688
+
689
+ if (query.isAsync !== undefined && query.isAsync) {
690
+ const asyncFns = [...this.functions.values()].filter(f => f.isAsync).map(f => f.id)
691
+ if (candidateIds) {
692
+ candidateIds = new Set([...candidateIds].filter(id => asyncFns.includes(id)))
693
+ } else {
694
+ candidateIds = new Set(asyncFns)
695
+ }
696
+ matchReasons.push('isAsync: true')
697
+ }
698
+
699
+ if (query.returnType) {
700
+ const ids = this.byReturnType.get(query.returnType.toLowerCase()) || []
701
+ if (candidateIds) {
702
+ candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
703
+ } else {
704
+ candidateIds = new Set(ids)
705
+ }
706
+ matchReasons.push(`returnType: ${query.returnType}`)
707
+ }
708
+
709
+ if (query.returnTypeContains) {
710
+ const lower = query.returnTypeContains.toLowerCase()
711
+ const matching = [...this.functions.values()]
712
+ .filter(f => f.returnType.toLowerCase().includes(lower))
713
+ .map(f => f.id)
714
+
715
+ if (candidateIds) {
716
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
717
+ } else {
718
+ candidateIds = new Set(matching)
719
+ }
720
+ matchReasons.push(`returnType contains: ${query.returnTypeContains}`)
721
+ }
722
+
723
+ if (query.hasParam) {
724
+ const matching = [...this.functions.values()]
725
+ .filter(f => f.params.some(p => p.name.toLowerCase() === query.hasParam!.toLowerCase()))
726
+ .map(f => f.id)
727
+
728
+ if (candidateIds) {
729
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
730
+ } else {
731
+ candidateIds = new Set(matching)
732
+ }
733
+ matchReasons.push(`has param: ${query.hasParam}`)
734
+ }
735
+
736
+ if (query.paramTypes && query.paramTypes.length > 0) {
737
+ const matching = [...this.functions.values()]
738
+ .filter(f => {
739
+ const paramTypeStr = f.params.map(p => p.type.toLowerCase()).join(',')
740
+ return query.paramTypes!.every(t => paramTypeStr.includes(t.toLowerCase()))
741
+ })
742
+ .map(f => f.id)
743
+
744
+ if (candidateIds) {
745
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
746
+ } else {
747
+ candidateIds = new Set(matching)
748
+ }
749
+ matchReasons.push(`param types: ${query.paramTypes.join(', ')}`)
750
+ }
751
+
752
+ if (query.hasDecorator) {
753
+ const lower = query.hasDecorator.toLowerCase()
754
+ const ids = this.byDecorator.get(lower) || []
755
+ if (candidateIds) {
756
+ candidateIds = new Set([...candidateIds].filter(id => ids.includes(id)))
757
+ } else {
758
+ candidateIds = new Set(ids)
759
+ }
760
+ matchReasons.push(`decorator: ${query.hasDecorator}`)
761
+ }
762
+
763
+ if (query.keyword) {
764
+ const lower = query.keyword.toLowerCase()
765
+ const ids = this.byKeyword.get(lower)
766
+ if (ids && ids.size > 0) {
767
+ if (candidateIds) {
768
+ candidateIds = new Set([...candidateIds].filter(id => ids.has(id)))
769
+ } else {
770
+ candidateIds = new Set(ids)
771
+ }
772
+ matchReasons.push(`keyword: ${query.keyword}`)
773
+ } else if (!candidateIds) {
774
+ candidateIds = new Set()
775
+ }
776
+ }
777
+
778
+ if (query.text) {
779
+ const tokens = query.text.toLowerCase().split(/\s+/).filter(t => t.length >= 2)
780
+ const tokenMatches = tokens.map(token => this.textIndex.get(token) || new Set())
781
+
782
+ const matchingIds = [...this.functions.keys()].filter(id => {
783
+ return tokenMatches.every(set => set.has(id))
784
+ })
785
+
786
+ if (candidateIds) {
787
+ candidateIds = new Set([...candidateIds].filter(id => matchingIds.includes(id)))
788
+ } else {
789
+ candidateIds = new Set(matchingIds)
790
+ }
791
+ matchReasons.push(`text search: ${query.text}`)
792
+ }
793
+
794
+ if (query.calls) {
795
+ const lower = query.calls.toLowerCase()
796
+ const ids = this.byCall.get(lower)
797
+ if (ids && ids.size > 0) {
798
+ if (candidateIds) {
799
+ candidateIds = new Set([...candidateIds].filter(id => ids.has(id)))
800
+ } else {
801
+ candidateIds = new Set(ids)
802
+ }
803
+ matchReasons.push(`calls: ${query.calls}`)
804
+ } else if (!candidateIds) {
805
+ candidateIds = new Set()
806
+ }
807
+ }
808
+
809
+ if (query.calledBy) {
810
+ const ids = this.byCalledBy.get(query.calledBy)
811
+ if (ids && ids.size > 0) {
812
+ if (candidateIds) {
813
+ candidateIds = new Set([...candidateIds].filter(id => ids.has(id)))
814
+ } else {
815
+ candidateIds = new Set(ids)
816
+ }
817
+ matchReasons.push(`calledBy: ${query.calledBy}`)
818
+ } else if (!candidateIds) {
819
+ candidateIds = new Set()
820
+ }
821
+ }
822
+
823
+ if (query.minParams !== undefined) {
824
+ const matching = [...this.functions.values()]
825
+ .filter(f => f.params.length >= query.minParams!)
826
+ .map(f => f.id)
827
+
828
+ if (candidateIds) {
829
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
830
+ } else {
831
+ candidateIds = new Set(matching)
832
+ }
833
+ matchReasons.push(`minParams: ${query.minParams}`)
834
+ }
835
+
836
+ if (query.maxParams !== undefined) {
837
+ const matching = [...this.functions.values()]
838
+ .filter(f => f.params.length <= query.maxParams!)
839
+ .map(f => f.id)
840
+
841
+ if (candidateIds) {
842
+ candidateIds = new Set([...candidateIds].filter(id => matching.includes(id)))
843
+ } else {
844
+ candidateIds = new Set(matching)
845
+ }
846
+ matchReasons.push(`maxParams: ${query.maxParams}`)
847
+ }
848
+
849
+ if (!candidateIds) {
850
+ candidateIds = new Set(this.functions.keys())
851
+ }
852
+
853
+ const results: SearchResult[] = []
854
+ for (const id of candidateIds) {
855
+ const fn = this.functions.get(id)
856
+ if (!fn) continue
857
+
858
+ let score = 1.0
859
+
860
+ if (query.name && fn.name.toLowerCase() === query.name.toLowerCase()) {
861
+ score *= 2.0
862
+ }
863
+ if (query.isExported !== undefined && fn.isExported === query.isExported) {
864
+ score *= 1.5
865
+ }
866
+ if (query.keyword) {
867
+ const kw = query.keyword.toLowerCase()
868
+ if (fn.keywords.includes(kw)) score *= 1.5
869
+ }
870
+
871
+ results.push({
872
+ function: fn,
873
+ score,
874
+ matchReasons,
875
+ })
876
+ }
877
+
878
+ results.sort((a, b) => b.score - a.score)
879
+
880
+ const offset = query.offset || 0
881
+ const limit = query.limit || 100
882
+
883
+ return results.slice(offset, offset + limit)
884
+ }
885
+
886
+ searchText(text: string, limit: number = 20): SearchResult[] {
887
+ return this.search({ text, limit })
888
+ }
889
+
890
+ searchByName(name: string, limit: number = 20): SearchResult[] {
891
+ return this.search({ nameContains: name, limit })
892
+ }
893
+
894
+ getCallers(functionId: string): RichFunction[] {
895
+ const fn = this.functions.get(functionId)
896
+ if (!fn) return []
897
+ return fn.calledBy
898
+ .map(id => this.functions.get(id))
899
+ .filter(Boolean) as RichFunction[]
900
+ }
901
+
902
+ getCallees(functionId: string): RichFunction[] {
903
+ const fn = this.functions.get(functionId)
904
+ if (!fn) return []
905
+ return fn.calls
906
+ .map(c => c.targetId)
907
+ .filter(Boolean)
908
+ .map(id => this.functions.get(id!))
909
+ .filter(Boolean) as RichFunction[]
910
+ }
911
+
912
+ getRelated(functionId: string, depth: number = 1): RichFunction[] {
913
+ const related = new Set<string>()
914
+ const queue: { id: string; d: number }[] = [{ id: functionId, d: 0 }]
915
+
916
+ while (queue.length > 0) {
917
+ const { id, d } = queue.shift()!
918
+ if (d >= depth) continue
919
+
920
+ const fn = this.functions.get(id)
921
+ if (!fn) continue
922
+
923
+ for (const callerId of fn.calledBy) {
924
+ if (!related.has(callerId)) {
925
+ related.add(callerId)
926
+ queue.push({ id: callerId, d: d + 1 })
927
+ }
928
+ }
929
+
930
+ for (const callee of fn.calls) {
931
+ if (callee.targetId && !related.has(callee.targetId)) {
932
+ related.add(callee.targetId)
933
+ queue.push({ id: callee.targetId, d: d + 1 })
934
+ }
935
+ }
936
+ }
937
+
938
+ return [...related]
939
+ .map(id => this.functions.get(id))
940
+ .filter(Boolean) as RichFunction[]
941
+ }
942
+
943
+ getContext(request: ContextRequest): FunctionContext | null {
944
+ const fn = this.functions.get(request.functionId)
945
+ if (!fn) return null
946
+
947
+ const include = request.include || 'full'
948
+
949
+ const context: FunctionContext = {
950
+ signature: fn.signature,
951
+ fullSignature: fn.fullSignature,
952
+ params: fn.params,
953
+ returnType: fn.returnType,
954
+ calls: include === 'signature' ? [] : fn.calls,
955
+ calledBy: include === 'signature' ? [] : fn.calledBy,
956
+ decorators: include === 'signature' ? [] : fn.decorators,
957
+ file: fn.file,
958
+ startLine: fn.startLine,
959
+ endLine: fn.endLine,
960
+ keywords: fn.keywords,
961
+ }
962
+
963
+ if (include === 'full' || include === 'body' || include === 'all') {
964
+ context.purpose = fn.purpose
965
+ context.docComment = fn.docComment
966
+ context.errorHandling = fn.errorHandling
967
+ context.edgeCases = fn.edgeCasesHandled
968
+ }
969
+
970
+ if (fn.body && (include === 'body' || include === 'all')) {
971
+ if (request.maxBodyLines && request.maxBodyLines > 0) {
972
+ const lines = fn.body.split('\n')
973
+ context.body = lines.slice(0, request.maxBodyLines).join('\n')
974
+ if (lines.length > request.maxBodyLines) {
975
+ context.body += `\n... ${lines.length - request.maxBodyLines} more lines`
976
+ }
977
+ } else {
978
+ context.body = fn.body
979
+ }
980
+ }
981
+
982
+ return context
983
+ }
984
+
985
+ getSignatures(functionIds: string[]): string[] {
986
+ return functionIds
987
+ .map(id => this.functions.get(id))
988
+ .filter(Boolean)
989
+ .map(fn => fn!.fullSignature)
990
+ }
991
+
992
+ getSignaturesMap(functionIds: string[]): Record<string, string> {
993
+ const map: Record<string, string> = {}
994
+ for (const id of functionIds) {
995
+ const fn = this.functions.get(id)
996
+ if (fn) {
997
+ map[id] = fn.fullSignature
998
+ }
999
+ }
1000
+ return map
1001
+ }
1002
+
1003
+ getSummaries(functionIds: string[]): Array<{ id: string; name: string; signature: string; purpose: string; file: string }> {
1004
+ return functionIds
1005
+ .map(id => {
1006
+ const fn = this.functions.get(id)
1007
+ if (!fn) return null
1008
+ return {
1009
+ id: fn.id,
1010
+ name: fn.name,
1011
+ signature: fn.fullSignature,
1012
+ purpose: fn.purpose,
1013
+ file: fn.file,
1014
+ }
1015
+ })
1016
+ .filter(Boolean) as any
1017
+ }
1018
+
1019
+ getAllSignatures(): Map<string, string> {
1020
+ const map = new Map<string, string>()
1021
+ for (const [id, fn] of this.functions) {
1022
+ map.set(id, fn.fullSignature)
1023
+ }
1024
+ return map
1025
+ }
1026
+
1027
+ getAllSummaries(): Array<{ id: string; name: string; signature: string; purpose: string; file: string }> {
1028
+ return [...this.functions.values()].map(fn => ({
1029
+ id: fn.id,
1030
+ name: fn.name,
1031
+ signature: fn.fullSignature,
1032
+ purpose: fn.purpose,
1033
+ file: fn.file,
1034
+ }))
1035
+ }
1036
+
1037
+ getKeywords(): string[] {
1038
+ return [...this.allKeywords]
1039
+ }
1040
+
1041
+ getStats(): {
1042
+ totalFunctions: number
1043
+ exportedCount: number
1044
+ asyncCount: number
1045
+ byModule: Record<string, number>
1046
+ byReturnType: Record<string, number>
1047
+ byFile: Record<string, number>
1048
+ } {
1049
+ const functions = [...this.functions.values()]
1050
+
1051
+ const byModule: Record<string, number> = {}
1052
+ const byReturnType: Record<string, number> = {}
1053
+ const byFile: Record<string, number> = {}
1054
+
1055
+ let exportedCount = 0
1056
+ let asyncCount = 0
1057
+
1058
+ for (const fn of functions) {
1059
+ if (fn.isExported) exportedCount++
1060
+ if (fn.isAsync) asyncCount++
1061
+
1062
+ byModule[fn.moduleId] = (byModule[fn.moduleId] || 0) + 1
1063
+
1064
+ const returnType = fn.returnType || 'unknown'
1065
+ byReturnType[returnType] = (byReturnType[returnType] || 0) + 1
1066
+
1067
+ const fileName = fn.file.split('/').pop() || fn.file
1068
+ byFile[fileName] = (byFile[fileName] || 0) + 1
1069
+ }
1070
+
1071
+ return {
1072
+ totalFunctions: functions.length,
1073
+ exportedCount,
1074
+ asyncCount,
1075
+ byModule,
1076
+ byReturnType,
1077
+ byFile,
1078
+ }
1079
+ }
1080
+ }