@getmikk/core 2.0.14 → 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 (64) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/src/analysis/type-flow.ts +1 -1
  4. package/src/cache/incremental-cache.ts +86 -80
  5. package/src/contract/contract-reader.ts +1 -0
  6. package/src/contract/lock-compiler.ts +95 -13
  7. package/src/contract/schema.ts +2 -0
  8. package/src/error-handler.ts +2 -1
  9. package/src/graph/cluster-detector.ts +2 -4
  10. package/src/graph/dead-code-detector.ts +303 -117
  11. package/src/graph/graph-builder.ts +21 -161
  12. package/src/graph/impact-analyzer.ts +1 -0
  13. package/src/graph/index.ts +2 -0
  14. package/src/graph/rich-function-index.ts +1080 -0
  15. package/src/graph/symbol-table.ts +252 -0
  16. package/src/hash/hash-store.ts +1 -0
  17. package/src/index.ts +2 -0
  18. package/src/parser/base-extractor.ts +19 -0
  19. package/src/parser/boundary-checker.ts +31 -12
  20. package/src/parser/error-recovery.ts +5 -4
  21. package/src/parser/function-body-extractor.ts +248 -0
  22. package/src/parser/go/go-extractor.ts +249 -676
  23. package/src/parser/index.ts +132 -318
  24. package/src/parser/language-registry.ts +57 -0
  25. package/src/parser/oxc-parser.ts +166 -28
  26. package/src/parser/oxc-resolver.ts +179 -11
  27. package/src/parser/parser-constants.ts +1 -0
  28. package/src/parser/rust/rust-extractor.ts +109 -0
  29. package/src/parser/tree-sitter/parser.ts +369 -62
  30. package/src/parser/tree-sitter/queries.ts +106 -10
  31. package/src/parser/types.ts +20 -1
  32. package/src/search/bm25.ts +21 -8
  33. package/src/search/direct-search.ts +472 -0
  34. package/src/search/embedding-provider.ts +249 -0
  35. package/src/search/index.ts +12 -0
  36. package/src/search/semantic-search.ts +435 -0
  37. package/src/utils/artifact-transaction.ts +1 -0
  38. package/src/utils/atomic-write.ts +1 -0
  39. package/src/utils/errors.ts +89 -4
  40. package/src/utils/fs.ts +104 -50
  41. package/src/utils/json.ts +1 -0
  42. package/src/utils/language-registry.ts +84 -6
  43. package/src/utils/path.ts +26 -0
  44. package/tests/dead-code.test.ts +3 -2
  45. package/tests/direct-search.test.ts +435 -0
  46. package/tests/error-recovery.test.ts +143 -0
  47. package/tests/fixtures/simple-api/src/index.ts +1 -1
  48. package/tests/go-parser.test.ts +19 -335
  49. package/tests/js-parser.test.ts +18 -1089
  50. package/tests/language-registry-all.test.ts +276 -0
  51. package/tests/language-registry.test.ts +6 -4
  52. package/tests/parse-diagnostics.test.ts +9 -96
  53. package/tests/parser.test.ts +42 -771
  54. package/tests/polyglot-parser.test.ts +117 -0
  55. package/tests/rich-function-index.test.ts +703 -0
  56. package/tests/tree-sitter-parser.test.ts +108 -80
  57. package/tests/ts-parser.test.ts +8 -8
  58. package/tests/verification.test.ts +175 -0
  59. package/src/parser/base-parser.ts +0 -16
  60. package/src/parser/go/go-parser.ts +0 -43
  61. package/src/parser/javascript/js-extractor.ts +0 -278
  62. package/src/parser/javascript/js-parser.ts +0 -101
  63. package/src/parser/typescript/ts-extractor.ts +0 -447
  64. package/src/parser/typescript/ts-parser.ts +0 -36
@@ -0,0 +1,703 @@
1
+ import { describe, it, expect, beforeEach } from 'bun:test'
2
+ import { RichFunctionIndex } from '../src/graph/rich-function-index'
3
+ import type { MikkLock } from '../src/contract/schema'
4
+
5
+ function createMockLock(functions: Partial<MikkLock['functions']> = {}): MikkLock {
6
+ return {
7
+ version: '1.0.0',
8
+ generatedAt: new Date().toISOString(),
9
+ generatorVersion: '1.0.0',
10
+ projectRoot: '/test',
11
+ syncState: {
12
+ status: 'clean',
13
+ lastSyncAt: new Date().toISOString(),
14
+ lockHash: 'x',
15
+ contractHash: 'x',
16
+ },
17
+ graph: { nodes: 0, edges: 0, rootHash: 'x' },
18
+ functions: functions as MikkLock['functions'],
19
+ classes: {},
20
+ files: {},
21
+ modules: {},
22
+ routes: [],
23
+ }
24
+ }
25
+
26
+ describe('RichFunctionIndex', () => {
27
+ let index: RichFunctionIndex
28
+
29
+ beforeEach(() => {
30
+ index = new RichFunctionIndex()
31
+ })
32
+
33
+ describe('indexing', () => {
34
+ it('indexes functions from lock', () => {
35
+ const lock = createMockLock({
36
+ 'fn:src/auth.ts:signToken': {
37
+ id: 'fn:src/auth.ts:signToken',
38
+ name: 'signToken',
39
+ file: 'src/auth.ts',
40
+ startLine: 10,
41
+ endLine: 25,
42
+ hash: 'abc123',
43
+ calls: [],
44
+ calledBy: [],
45
+ moduleId: 'auth',
46
+ },
47
+ })
48
+
49
+ index.index(lock)
50
+ expect(index.getCount()).toBe(1)
51
+ })
52
+
53
+ it('handles empty lock', () => {
54
+ const lock = createMockLock()
55
+ index.index(lock)
56
+ expect(index.getCount()).toBe(0)
57
+ })
58
+
59
+ it('parses function ids correctly', () => {
60
+ const lock = createMockLock({
61
+ 'fn:src/utils/helper.ts:parseData': {
62
+ id: 'fn:src/utils/helper.ts:parseData',
63
+ name: 'parseData',
64
+ file: 'src/utils/helper.ts',
65
+ startLine: 1,
66
+ endLine: 10,
67
+ hash: 'hash1',
68
+ calls: [],
69
+ calledBy: [],
70
+ moduleId: 'utils',
71
+ },
72
+ })
73
+
74
+ index.index(lock)
75
+ const fn = index.get('fn:src/utils/helper.ts:parseData')
76
+ expect(fn).toBeDefined()
77
+ expect(fn?.name).toBe('parseData')
78
+ })
79
+ })
80
+
81
+ describe('getByExactName', () => {
82
+ it('finds function by exact name', () => {
83
+ const lock = createMockLock({
84
+ 'fn:src/auth.ts:verifyToken': {
85
+ id: 'fn:src/auth.ts:verifyToken',
86
+ name: 'verifyToken',
87
+ file: 'src/auth.ts',
88
+ startLine: 1,
89
+ endLine: 10,
90
+ hash: 'x',
91
+ calls: [],
92
+ calledBy: [],
93
+ moduleId: 'auth',
94
+ },
95
+ })
96
+
97
+ index.index(lock)
98
+ const fn = index.getByExactName('verifyToken')
99
+ expect(fn).toBeDefined()
100
+ expect(fn?.id).toBe('fn:src/auth.ts:verifyToken')
101
+ })
102
+
103
+ it('returns undefined for non-existent name', () => {
104
+ const lock = createMockLock()
105
+ index.index(lock)
106
+ expect(index.getByExactName('nonExistent')).toBeUndefined()
107
+ })
108
+ })
109
+
110
+ describe('getByFile', () => {
111
+ it('finds all functions in a file', () => {
112
+ const lock = createMockLock({
113
+ 'fn:src/auth.ts:login': {
114
+ id: 'fn:src/auth.ts:login',
115
+ name: 'login',
116
+ file: 'src/auth.ts',
117
+ startLine: 1,
118
+ endLine: 10,
119
+ hash: 'x',
120
+ calls: [],
121
+ calledBy: [],
122
+ moduleId: 'auth',
123
+ },
124
+ 'fn:src/auth.ts:logout': {
125
+ id: 'fn:src/auth.ts:logout',
126
+ name: 'logout',
127
+ file: 'src/auth.ts',
128
+ startLine: 12,
129
+ endLine: 20,
130
+ hash: 'y',
131
+ calls: [],
132
+ calledBy: [],
133
+ moduleId: 'auth',
134
+ },
135
+ 'fn:src/other.ts:helper': {
136
+ id: 'fn:src/other.ts:helper',
137
+ name: 'helper',
138
+ file: 'src/other.ts',
139
+ startLine: 1,
140
+ endLine: 5,
141
+ hash: 'z',
142
+ calls: [],
143
+ calledBy: [],
144
+ moduleId: 'other',
145
+ },
146
+ })
147
+
148
+ index.index(lock)
149
+ const fns = index.getByFile('src/auth.ts')
150
+ expect(fns).toHaveLength(2)
151
+ expect(fns.map(f => f.name).sort()).toEqual(['login', 'logout'])
152
+ })
153
+
154
+ it('returns empty array for non-existent file', () => {
155
+ const lock = createMockLock()
156
+ index.index(lock)
157
+ expect(index.getByFile('nonExistent.ts')).toHaveLength(0)
158
+ })
159
+ })
160
+
161
+ describe('getByModule', () => {
162
+ it('finds all functions in a module', () => {
163
+ const lock = createMockLock({
164
+ 'fn:src/auth/login.ts:login': {
165
+ id: 'fn:src/auth/login.ts:login',
166
+ name: 'login',
167
+ file: 'src/auth/login.ts',
168
+ startLine: 1,
169
+ endLine: 10,
170
+ hash: 'x',
171
+ calls: [],
172
+ calledBy: [],
173
+ moduleId: 'auth',
174
+ },
175
+ 'fn:src/auth/jwt.ts:createToken': {
176
+ id: 'fn:src/auth/jwt.ts:createToken',
177
+ name: 'createToken',
178
+ file: 'src/auth/jwt.ts',
179
+ startLine: 1,
180
+ endLine: 10,
181
+ hash: 'y',
182
+ calls: [],
183
+ calledBy: [],
184
+ moduleId: 'auth',
185
+ },
186
+ })
187
+
188
+ index.index(lock)
189
+ const fns = index.getByModule('auth')
190
+ expect(fns).toHaveLength(2)
191
+ })
192
+ })
193
+
194
+ describe('getExported', () => {
195
+ it('returns only exported functions', () => {
196
+ const lock = createMockLock({
197
+ 'fn:src/auth.ts:publicFn': {
198
+ id: 'fn:src/auth.ts:publicFn',
199
+ name: 'publicFn',
200
+ file: 'src/auth.ts',
201
+ startLine: 1,
202
+ endLine: 10,
203
+ hash: 'x',
204
+ calls: [],
205
+ calledBy: [],
206
+ moduleId: 'auth',
207
+ isExported: true,
208
+ },
209
+ 'fn:src/auth.ts:privateFn': {
210
+ id: 'fn:src/auth.ts:privateFn',
211
+ name: 'privateFn',
212
+ file: 'src/auth.ts',
213
+ startLine: 11,
214
+ endLine: 20,
215
+ hash: 'y',
216
+ calls: [],
217
+ calledBy: [],
218
+ moduleId: 'auth',
219
+ isExported: false,
220
+ },
221
+ })
222
+
223
+ index.index(lock)
224
+ const exported = index.getExported()
225
+ expect(exported).toHaveLength(1)
226
+ expect(exported[0].name).toBe('publicFn')
227
+ })
228
+ })
229
+
230
+ describe('search', () => {
231
+ it('searches by name contains', () => {
232
+ const lock = createMockLock({
233
+ 'fn:src/auth.ts:getUser': {
234
+ id: 'fn:src/auth.ts:getUser',
235
+ name: 'getUser',
236
+ file: 'src/auth.ts',
237
+ startLine: 1,
238
+ endLine: 10,
239
+ hash: 'x',
240
+ calls: [],
241
+ calledBy: [],
242
+ moduleId: 'auth',
243
+ },
244
+ 'fn:src/auth.ts:createUser': {
245
+ id: 'fn:src/auth.ts:createUser',
246
+ name: 'createUser',
247
+ file: 'src/auth.ts',
248
+ startLine: 11,
249
+ endLine: 20,
250
+ hash: 'y',
251
+ calls: [],
252
+ calledBy: [],
253
+ moduleId: 'auth',
254
+ },
255
+ 'fn:src/db.ts:query': {
256
+ id: 'fn:src/db.ts:query',
257
+ name: 'query',
258
+ file: 'src/db.ts',
259
+ startLine: 1,
260
+ endLine: 10,
261
+ hash: 'z',
262
+ calls: [],
263
+ calledBy: [],
264
+ moduleId: 'db',
265
+ },
266
+ })
267
+
268
+ index.index(lock)
269
+ const results = index.search({ nameContains: 'user' })
270
+ expect(results).toHaveLength(2)
271
+ expect(results.map(r => r.function.name).sort()).toEqual(['createUser', 'getUser'])
272
+ })
273
+
274
+ it('searches by exact name', () => {
275
+ const lock = createMockLock({
276
+ 'fn:src/auth.ts:login': {
277
+ id: 'fn:src/auth.ts:login',
278
+ name: 'login',
279
+ file: 'src/auth.ts',
280
+ startLine: 1,
281
+ endLine: 10,
282
+ hash: 'x',
283
+ calls: [],
284
+ calledBy: [],
285
+ moduleId: 'auth',
286
+ },
287
+ })
288
+
289
+ index.index(lock)
290
+ const results = index.search({ exactName: 'login' })
291
+ expect(results).toHaveLength(1)
292
+ expect(results[0].function.name).toBe('login')
293
+ })
294
+
295
+ it('searches by isAsync', () => {
296
+ const lock = createMockLock({
297
+ 'fn:src/auth.ts:syncFn': {
298
+ id: 'fn:src/auth.ts:syncFn',
299
+ name: 'syncFn',
300
+ file: 'src/auth.ts',
301
+ startLine: 1,
302
+ endLine: 10,
303
+ hash: 'x',
304
+ calls: [],
305
+ calledBy: [],
306
+ moduleId: 'auth',
307
+ isAsync: false,
308
+ },
309
+ 'fn:src/db.ts:asyncFn': {
310
+ id: 'fn:src/db.ts:asyncFn',
311
+ name: 'asyncFn',
312
+ file: 'src/db.ts',
313
+ startLine: 1,
314
+ endLine: 10,
315
+ hash: 'y',
316
+ calls: [],
317
+ calledBy: [],
318
+ moduleId: 'db',
319
+ isAsync: true,
320
+ },
321
+ })
322
+
323
+ index.index(lock)
324
+ const results = index.search({ isAsync: true })
325
+ expect(results).toHaveLength(1)
326
+ expect(results[0].function.name).toBe('asyncFn')
327
+ })
328
+
329
+ it('searches by return type', () => {
330
+ const lock = createMockLock({
331
+ 'fn:src/auth.ts:getUser': {
332
+ id: 'fn:src/auth.ts:getUser',
333
+ name: 'getUser',
334
+ file: 'src/auth.ts',
335
+ startLine: 1,
336
+ endLine: 10,
337
+ hash: 'x',
338
+ calls: [],
339
+ calledBy: [],
340
+ moduleId: 'auth',
341
+ returnType: 'User',
342
+ },
343
+ 'fn:src/db.ts:delete': {
344
+ id: 'fn:src/db.ts:delete',
345
+ name: 'delete',
346
+ file: 'src/db.ts',
347
+ startLine: 1,
348
+ endLine: 10,
349
+ hash: 'y',
350
+ calls: [],
351
+ calledBy: [],
352
+ moduleId: 'db',
353
+ returnType: 'void',
354
+ },
355
+ })
356
+
357
+ index.index(lock)
358
+ const results = index.search({ returnType: 'void' })
359
+ expect(results).toHaveLength(1)
360
+ expect(results[0].function.name).toBe('delete')
361
+ })
362
+
363
+ it('combines multiple search criteria', () => {
364
+ const lock = createMockLock({
365
+ 'fn:src/auth.ts:asyncLogin': {
366
+ id: 'fn:src/auth.ts:asyncLogin',
367
+ name: 'asyncLogin',
368
+ file: 'src/auth.ts',
369
+ startLine: 1,
370
+ endLine: 10,
371
+ hash: 'x',
372
+ calls: [],
373
+ calledBy: [],
374
+ moduleId: 'auth',
375
+ isAsync: true,
376
+ },
377
+ 'fn:src/db.ts:syncLogin': {
378
+ id: 'fn:src/db.ts:syncLogin',
379
+ name: 'syncLogin',
380
+ file: 'src/db.ts',
381
+ startLine: 1,
382
+ endLine: 10,
383
+ hash: 'y',
384
+ calls: [],
385
+ calledBy: [],
386
+ moduleId: 'db',
387
+ isAsync: false,
388
+ },
389
+ })
390
+
391
+ index.index(lock)
392
+ const results = index.search({ nameContains: 'login', isAsync: true })
393
+ expect(results).toHaveLength(1)
394
+ expect(results[0].function.name).toBe('asyncLogin')
395
+ })
396
+ })
397
+
398
+ describe('searchText', () => {
399
+ it('searches across multiple fields', () => {
400
+ const lock = createMockLock({
401
+ 'fn:src/auth.ts:login': {
402
+ id: 'fn:src/auth.ts:login',
403
+ name: 'login',
404
+ file: 'src/auth.ts',
405
+ startLine: 1,
406
+ endLine: 10,
407
+ hash: 'x',
408
+ calls: [],
409
+ calledBy: [],
410
+ moduleId: 'auth',
411
+ purpose: 'Handles user authentication',
412
+ },
413
+ })
414
+
415
+ index.index(lock)
416
+ const results = index.searchText('authentication')
417
+ expect(results).toHaveLength(1)
418
+ expect(results[0].function.name).toBe('login')
419
+ })
420
+ })
421
+
422
+ describe('findByLocation', () => {
423
+ it('finds function by file and line', () => {
424
+ const lock = createMockLock({
425
+ 'fn:src/auth.ts:login': {
426
+ id: 'fn:src/auth.ts:login',
427
+ name: 'login',
428
+ file: 'src/auth.ts',
429
+ startLine: 10,
430
+ endLine: 25,
431
+ hash: 'x',
432
+ calls: [],
433
+ calledBy: [],
434
+ moduleId: 'auth',
435
+ },
436
+ })
437
+
438
+ index.index(lock)
439
+ const fn = index.findByLocation('src/auth.ts', 15)
440
+ expect(fn?.name).toBe('login')
441
+ })
442
+
443
+ it('returns undefined for line outside function range', () => {
444
+ const lock = createMockLock({
445
+ 'fn:src/auth.ts:login': {
446
+ id: 'fn:src/auth.ts:login',
447
+ name: 'login',
448
+ file: 'src/auth.ts',
449
+ startLine: 10,
450
+ endLine: 25,
451
+ hash: 'x',
452
+ calls: [],
453
+ calledBy: [],
454
+ moduleId: 'auth',
455
+ },
456
+ })
457
+
458
+ index.index(lock)
459
+ const fn = index.findByLocation('src/auth.ts', 100)
460
+ expect(fn).toBeUndefined()
461
+ })
462
+ })
463
+
464
+ describe('getCallers / getCallees', () => {
465
+ it('gets callers of a function', () => {
466
+ const lock = createMockLock({
467
+ 'fn:src/auth.ts:login': {
468
+ id: 'fn:src/auth.ts:login',
469
+ name: 'login',
470
+ file: 'src/auth.ts',
471
+ startLine: 1,
472
+ endLine: 10,
473
+ hash: 'x',
474
+ calls: [],
475
+ calledBy: ['fn:src/api.ts:handleLogin'],
476
+ moduleId: 'auth',
477
+ },
478
+ 'fn:src/api.ts:handleLogin': {
479
+ id: 'fn:src/api.ts:handleLogin',
480
+ name: 'handleLogin',
481
+ file: 'src/api.ts',
482
+ startLine: 1,
483
+ endLine: 10,
484
+ hash: 'y',
485
+ calls: ['fn:src/auth.ts:login'],
486
+ calledBy: [],
487
+ moduleId: 'api',
488
+ },
489
+ })
490
+
491
+ index.index(lock)
492
+ const callers = index.getCallers('fn:src/auth.ts:login')
493
+ expect(callers).toHaveLength(1)
494
+ expect(callers[0].name).toBe('handleLogin')
495
+ })
496
+
497
+ it('gets callees of a function (by calledBy reverse lookup)', () => {
498
+ const lock = createMockLock({
499
+ 'fn:src/auth.ts:login': {
500
+ id: 'fn:src/auth.ts:login',
501
+ name: 'login',
502
+ file: 'src/auth.ts',
503
+ startLine: 1,
504
+ endLine: 10,
505
+ hash: 'x',
506
+ calls: [],
507
+ calledBy: [],
508
+ moduleId: 'auth',
509
+ },
510
+ 'fn:src/db.ts:query': {
511
+ id: 'fn:src/db.ts:query',
512
+ name: 'query',
513
+ file: 'src/db.ts',
514
+ startLine: 1,
515
+ endLine: 10,
516
+ hash: 'y',
517
+ calls: [],
518
+ calledBy: ['fn:src/auth.ts:login'],
519
+ moduleId: 'db',
520
+ },
521
+ })
522
+
523
+ index.index(lock)
524
+ // getCallees returns functions that the given function calls
525
+ // Since login doesn't call query directly, we test via getCallers
526
+ const callers = index.getCallers('fn:src/db.ts:query')
527
+ expect(callers.some(c => c.name === 'login')).toBe(true)
528
+ })
529
+ })
530
+
531
+ describe('getStats', () => {
532
+ it('returns correct statistics', () => {
533
+ const lock = createMockLock({
534
+ 'fn:src/auth.ts:login': {
535
+ id: 'fn:src/auth.ts:login',
536
+ name: 'login',
537
+ file: 'src/auth.ts',
538
+ startLine: 1,
539
+ endLine: 10,
540
+ hash: 'x',
541
+ calls: [],
542
+ calledBy: [],
543
+ moduleId: 'auth',
544
+ isExported: true,
545
+ isAsync: true,
546
+ },
547
+ 'fn:src/db.ts:query': {
548
+ id: 'fn:src/db.ts:query',
549
+ name: 'query',
550
+ file: 'src/db.ts',
551
+ startLine: 1,
552
+ endLine: 10,
553
+ hash: 'y',
554
+ calls: [],
555
+ calledBy: [],
556
+ moduleId: 'db',
557
+ isExported: false,
558
+ isAsync: false,
559
+ },
560
+ })
561
+
562
+ index.index(lock)
563
+ const stats = index.getStats()
564
+ expect(stats.totalFunctions).toBe(2)
565
+ expect(stats.exportedCount).toBe(1)
566
+ expect(stats.asyncCount).toBe(1)
567
+ expect(stats.byModule.auth).toBe(1)
568
+ expect(stats.byModule.db).toBe(1)
569
+ })
570
+ })
571
+
572
+ describe('getContext', () => {
573
+ it('returns function context', () => {
574
+ const lock = createMockLock({
575
+ 'fn:src/auth.ts:login': {
576
+ id: 'fn:src/auth.ts:login',
577
+ name: 'login',
578
+ file: 'src/auth.ts',
579
+ startLine: 1,
580
+ endLine: 10,
581
+ hash: 'x',
582
+ calls: [],
583
+ calledBy: [],
584
+ moduleId: 'auth',
585
+ params: [{ name: 'email', type: 'string' }],
586
+ returnType: 'User',
587
+ },
588
+ })
589
+
590
+ index.index(lock)
591
+ const ctx = index.getContext({ functionId: 'fn:src/auth.ts:login' })
592
+ expect(ctx).toBeDefined()
593
+ expect(ctx?.signature).toContain('login')
594
+ expect(ctx?.params).toHaveLength(1)
595
+ expect(ctx?.params[0].name).toBe('email')
596
+ })
597
+
598
+ it('returns null for non-existent function', () => {
599
+ const lock = createMockLock()
600
+ index.index(lock)
601
+ const ctx = index.getContext({ functionId: 'nonExistent' })
602
+ expect(ctx).toBeNull()
603
+ })
604
+ })
605
+
606
+ describe('signature building', () => {
607
+ it('builds correct signatures', () => {
608
+ const lock = createMockLock({
609
+ 'fn:src/auth.ts:login': {
610
+ id: 'fn:src/auth.ts:login',
611
+ name: 'login',
612
+ file: 'src/auth.ts',
613
+ startLine: 1,
614
+ endLine: 10,
615
+ hash: 'x',
616
+ calls: [],
617
+ calledBy: [],
618
+ moduleId: 'auth',
619
+ params: [
620
+ { name: 'email', type: 'string' },
621
+ { name: 'password', type: 'string', optional: true },
622
+ ],
623
+ returnType: 'User',
624
+ isAsync: true,
625
+ },
626
+ })
627
+
628
+ index.index(lock)
629
+ const fn = index.get('fn:src/auth.ts:login')
630
+ expect(fn?.signature).toContain('login')
631
+ expect(fn?.signature).toContain('email')
632
+ expect(fn?.fullSignature).toContain('async')
633
+ expect(fn?.fullSignature).toContain(': User')
634
+ })
635
+ })
636
+
637
+ describe('purpose inference', () => {
638
+ it('infers purpose from function name', () => {
639
+ const lock = createMockLock({
640
+ 'fn:src/db.ts:getUser': {
641
+ id: 'fn:src/db.ts:getUser',
642
+ name: 'getUser',
643
+ file: 'src/db.ts',
644
+ startLine: 1,
645
+ endLine: 10,
646
+ hash: 'x',
647
+ calls: [],
648
+ calledBy: [],
649
+ moduleId: 'db',
650
+ },
651
+ })
652
+
653
+ index.index(lock)
654
+ const fn = index.get('fn:src/db.ts:getUser')
655
+ expect(fn?.purpose).toContain('Retrieves')
656
+ })
657
+ })
658
+
659
+ describe('keyword extraction', () => {
660
+ it('extracts keywords from function name', () => {
661
+ const lock = createMockLock({
662
+ 'fn:src/auth.ts:validateUserEmail': {
663
+ id: 'fn:src/auth.ts:validateUserEmail',
664
+ name: 'validateUserEmail',
665
+ file: 'src/auth.ts',
666
+ startLine: 1,
667
+ endLine: 10,
668
+ hash: 'x',
669
+ calls: [],
670
+ calledBy: [],
671
+ moduleId: 'auth',
672
+ },
673
+ })
674
+
675
+ index.index(lock)
676
+ const fn = index.get('fn:src/auth.ts:validateUserEmail')
677
+ expect(fn?.keywords).toContain('validate')
678
+ expect(fn?.keywords).toContain('user')
679
+ expect(fn?.keywords).toContain('email')
680
+ })
681
+
682
+ it('marks async functions with async keyword', () => {
683
+ const lock = createMockLock({
684
+ 'fn:src/db.ts:query': {
685
+ id: 'fn:src/db.ts:query',
686
+ name: 'query',
687
+ file: 'src/db.ts',
688
+ startLine: 1,
689
+ endLine: 10,
690
+ hash: 'x',
691
+ calls: [],
692
+ calledBy: [],
693
+ moduleId: 'db',
694
+ isAsync: true,
695
+ },
696
+ })
697
+
698
+ index.index(lock)
699
+ const fn = index.get('fn:src/db.ts:query')
700
+ expect(fn?.keywords).toContain('async')
701
+ })
702
+ })
703
+ })