agentmap 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +24 -0
  3. package/dist/cli.js +44 -12
  4. package/dist/cli.js.map +1 -1
  5. package/dist/extract/definitions.js +12 -12
  6. package/dist/extract/definitions.js.map +1 -1
  7. package/dist/extract/definitions.test.js +30 -259
  8. package/dist/extract/definitions.test.js.map +1 -1
  9. package/dist/extract/git-status.d.ts +11 -4
  10. package/dist/extract/git-status.d.ts.map +1 -1
  11. package/dist/extract/git-status.js +21 -16
  12. package/dist/extract/git-status.js.map +1 -1
  13. package/dist/extract/markdown.js +1 -1
  14. package/dist/extract/markdown.test.js +3 -3
  15. package/dist/extract/markdown.test.js.map +1 -1
  16. package/dist/extract/marker.js +1 -1
  17. package/dist/extract/marker.test.js +4 -4
  18. package/dist/extract/marker.test.js.map +1 -1
  19. package/dist/extract/submodules.d.ts +12 -0
  20. package/dist/extract/submodules.d.ts.map +1 -0
  21. package/dist/extract/submodules.js +234 -0
  22. package/dist/extract/submodules.js.map +1 -0
  23. package/dist/extract/submodules.test.d.ts +2 -0
  24. package/dist/extract/submodules.test.d.ts.map +1 -0
  25. package/dist/extract/submodules.test.js +84 -0
  26. package/dist/extract/submodules.test.js.map +1 -0
  27. package/dist/index.d.ts +4 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +10 -9
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger.d.ts +10 -0
  32. package/dist/logger.d.ts.map +1 -0
  33. package/dist/logger.js +41 -0
  34. package/dist/logger.js.map +1 -0
  35. package/dist/map/builder.d.ts +3 -3
  36. package/dist/map/builder.d.ts.map +1 -1
  37. package/dist/map/builder.js +59 -9
  38. package/dist/map/builder.js.map +1 -1
  39. package/dist/map/builder.test.d.ts +2 -0
  40. package/dist/map/builder.test.d.ts.map +1 -0
  41. package/dist/map/builder.test.js +66 -0
  42. package/dist/map/builder.test.js.map +1 -0
  43. package/dist/map/truncate.d.ts +7 -3
  44. package/dist/map/truncate.d.ts.map +1 -1
  45. package/dist/map/truncate.js +90 -9
  46. package/dist/map/truncate.js.map +1 -1
  47. package/dist/map/yaml.d.ts.map +1 -1
  48. package/dist/map/yaml.js +13 -3
  49. package/dist/map/yaml.js.map +1 -1
  50. package/dist/scanner.d.ts +9 -2
  51. package/dist/scanner.d.ts.map +1 -1
  52. package/dist/scanner.js +172 -49
  53. package/dist/scanner.js.map +1 -1
  54. package/dist/scanner.test.d.ts +2 -0
  55. package/dist/scanner.test.d.ts.map +1 -0
  56. package/dist/scanner.test.js +84 -0
  57. package/dist/scanner.test.js.map +1 -0
  58. package/dist/test-helpers/git-test-helpers.d.ts +13 -0
  59. package/dist/test-helpers/git-test-helpers.d.ts.map +1 -0
  60. package/dist/test-helpers/git-test-helpers.js +48 -0
  61. package/dist/test-helpers/git-test-helpers.js.map +1 -0
  62. package/dist/types.d.ts +42 -2
  63. package/dist/types.d.ts.map +1 -1
  64. package/package.json +15 -3
  65. package/src/cli.ts +164 -0
  66. package/src/extract/definitions.test.ts +2040 -0
  67. package/src/extract/definitions.ts +379 -0
  68. package/src/extract/git-status.test.ts +507 -0
  69. package/src/extract/git-status.ts +359 -0
  70. package/src/extract/markdown.test.ts +159 -0
  71. package/src/extract/markdown.ts +202 -0
  72. package/src/extract/marker.test.ts +566 -0
  73. package/src/extract/marker.ts +398 -0
  74. package/src/extract/submodules.test.ts +95 -0
  75. package/src/extract/submodules.ts +269 -0
  76. package/src/extract/utils.ts +27 -0
  77. package/src/index.ts +106 -0
  78. package/src/languages/cpp.ts +129 -0
  79. package/src/languages/go.ts +72 -0
  80. package/src/languages/index.ts +231 -0
  81. package/src/languages/javascript.ts +33 -0
  82. package/src/languages/python.ts +41 -0
  83. package/src/languages/rust.ts +72 -0
  84. package/src/languages/typescript.ts +74 -0
  85. package/src/languages/zig.ts +106 -0
  86. package/src/logger.ts +55 -0
  87. package/src/map/builder.test.ts +72 -0
  88. package/src/map/builder.ts +175 -0
  89. package/src/map/truncate.ts +188 -0
  90. package/src/map/yaml.ts +66 -0
  91. package/src/parser/index.ts +53 -0
  92. package/src/parser/languages.ts +64 -0
  93. package/src/scanner.test.ts +95 -0
  94. package/src/scanner.ts +364 -0
  95. package/src/test-helpers/git-test-helpers.ts +62 -0
  96. package/src/types.ts +191 -0
@@ -0,0 +1,507 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ parseDiff,
4
+ parseNumstat,
5
+ parseHunkHeader,
6
+ calculateDefinitionDiff,
7
+ calculateFileDiff
8
+ } from './git-status.js'
9
+ import type { Definition, DiffHunk } from '../types.js'
10
+
11
+ // ============================================================================
12
+ // parseNumstat - Machine-readable file stats (most reliable)
13
+ // ============================================================================
14
+
15
+ describe('parseNumstat', () => {
16
+ test('parses simple numstat output', () => {
17
+ const output = `10\t5\tsrc/foo.ts
18
+ 3\t1\tsrc/bar.ts`
19
+ const result = parseNumstat(output)
20
+ expect(result.size).toBe(2)
21
+ expect(result.get('src/foo.ts')).toMatchInlineSnapshot(`
22
+ {
23
+ "added": 10,
24
+ "deleted": 5,
25
+ }
26
+ `)
27
+ expect(result.get('src/bar.ts')).toMatchInlineSnapshot(`
28
+ {
29
+ "added": 3,
30
+ "deleted": 1,
31
+ }
32
+ `)
33
+ })
34
+
35
+ test('handles additions only', () => {
36
+ const output = `15\t0\tsrc/new-file.ts`
37
+ const result = parseNumstat(output)
38
+ expect(result.get('src/new-file.ts')).toMatchInlineSnapshot(`
39
+ {
40
+ "added": 15,
41
+ "deleted": 0,
42
+ }
43
+ `)
44
+ })
45
+
46
+ test('handles deletions only', () => {
47
+ const output = `0\t20\tsrc/deleted-content.ts`
48
+ const result = parseNumstat(output)
49
+ expect(result.get('src/deleted-content.ts')).toMatchInlineSnapshot(`
50
+ {
51
+ "added": 0,
52
+ "deleted": 20,
53
+ }
54
+ `)
55
+ })
56
+
57
+ test('skips binary files (shown as - -)', () => {
58
+ const output = `-\t-\timage.png
59
+ 10\t5\tsrc/code.ts`
60
+ const result = parseNumstat(output)
61
+ expect(result.size).toBe(1)
62
+ expect(result.has('image.png')).toBe(false)
63
+ expect(result.has('src/code.ts')).toBe(true)
64
+ })
65
+
66
+ test('handles empty output', () => {
67
+ const result = parseNumstat('')
68
+ expect(result.size).toBe(0)
69
+ })
70
+
71
+ test('handles whitespace-only output', () => {
72
+ const result = parseNumstat(' \n\n ')
73
+ expect(result.size).toBe(0)
74
+ })
75
+
76
+ test('skips malformed lines', () => {
77
+ const output = `not valid
78
+ 10\t5\tsrc/valid.ts
79
+ also invalid line`
80
+ const result = parseNumstat(output)
81
+ expect(result.size).toBe(1)
82
+ expect(result.has('src/valid.ts')).toBe(true)
83
+ })
84
+
85
+ test('handles paths with spaces', () => {
86
+ const output = `5\t3\tpath/with spaces/file.ts`
87
+ const result = parseNumstat(output)
88
+ expect(result.has('path/with spaces/file.ts')).toBe(true)
89
+ })
90
+
91
+ test('normalizes Windows backslashes to forward slashes', () => {
92
+ const output = `5\t3\tpath\\to\\file.ts`
93
+ const result = parseNumstat(output)
94
+ expect(result.has('path/to/file.ts')).toBe(true)
95
+ })
96
+
97
+ test('handles quoted paths (special characters)', () => {
98
+ const output = `5\t3\t"path/with\\"quotes\\"/file.ts"`
99
+ const result = parseNumstat(output)
100
+ // The path should be unquoted and escapes resolved
101
+ expect(result.size).toBe(1)
102
+ })
103
+
104
+ test('skips files with zero changes', () => {
105
+ const output = `0\t0\tsrc/unchanged.ts
106
+ 5\t3\tsrc/changed.ts`
107
+ const result = parseNumstat(output)
108
+ expect(result.size).toBe(1)
109
+ expect(result.has('src/unchanged.ts')).toBe(false)
110
+ expect(result.has('src/changed.ts')).toBe(true)
111
+ })
112
+ })
113
+
114
+ // ============================================================================
115
+ // parseHunkHeader - Extract line numbers from @@ headers
116
+ // ============================================================================
117
+
118
+ describe('parseHunkHeader', () => {
119
+ test('parses standard hunk header', () => {
120
+ const result = parseHunkHeader('@@ -10,5 +12,7 @@ function name() {')
121
+ expect(result).toMatchInlineSnapshot(`
122
+ {
123
+ "newCount": 7,
124
+ "newStart": 12,
125
+ "oldCount": 5,
126
+ "oldStart": 10,
127
+ }
128
+ `)
129
+ })
130
+
131
+ test('parses hunk with single old line (no comma)', () => {
132
+ const result = parseHunkHeader('@@ -10 +12,7 @@')
133
+ expect(result).toMatchInlineSnapshot(`
134
+ {
135
+ "newCount": 7,
136
+ "newStart": 12,
137
+ "oldCount": 1,
138
+ "oldStart": 10,
139
+ }
140
+ `)
141
+ })
142
+
143
+ test('parses hunk with single new line (no comma)', () => {
144
+ const result = parseHunkHeader('@@ -10,5 +12 @@')
145
+ expect(result).toMatchInlineSnapshot(`
146
+ {
147
+ "newCount": 1,
148
+ "newStart": 12,
149
+ "oldCount": 5,
150
+ "oldStart": 10,
151
+ }
152
+ `)
153
+ })
154
+
155
+ test('parses hunk with both single lines', () => {
156
+ const result = parseHunkHeader('@@ -1 +1 @@')
157
+ expect(result).toMatchInlineSnapshot(`
158
+ {
159
+ "newCount": 1,
160
+ "newStart": 1,
161
+ "oldCount": 1,
162
+ "oldStart": 1,
163
+ }
164
+ `)
165
+ })
166
+
167
+ test('returns null for invalid header', () => {
168
+ expect(parseHunkHeader('not a hunk')).toBeNull()
169
+ expect(parseHunkHeader('@@@ invalid @@@')).toBeNull()
170
+ expect(parseHunkHeader('')).toBeNull()
171
+ })
172
+
173
+ test('handles zero counts', () => {
174
+ const result = parseHunkHeader('@@ -10,0 +10,5 @@')
175
+ expect(result).toMatchInlineSnapshot(`
176
+ {
177
+ "newCount": 5,
178
+ "newStart": 10,
179
+ "oldCount": 0,
180
+ "oldStart": 10,
181
+ }
182
+ `)
183
+ })
184
+ })
185
+
186
+ // ============================================================================
187
+ // parseDiff - Extract hunks from full diff output
188
+ // ============================================================================
189
+
190
+ describe('parseDiff', () => {
191
+ test('parses single file with one hunk', () => {
192
+ const diffOutput = `diff --git a/src/foo.ts b/src/foo.ts
193
+ index abc123..def456 100644
194
+ --- a/src/foo.ts
195
+ +++ b/src/foo.ts
196
+ @@ -10,3 +10,5 @@ function existing() {
197
+ + const x = 1
198
+ + const y = 2
199
+ `
200
+ const result = parseDiff(diffOutput)
201
+ expect(result.size).toBe(1)
202
+ expect(result.get('src/foo.ts')).toMatchInlineSnapshot(`
203
+ {
204
+ "hunks": [
205
+ {
206
+ "newCount": 5,
207
+ "newStart": 10,
208
+ "oldCount": 3,
209
+ "oldStart": 10,
210
+ },
211
+ ],
212
+ "path": "src/foo.ts",
213
+ }
214
+ `)
215
+ })
216
+
217
+ test('parses single file with multiple hunks', () => {
218
+ const diffOutput = `diff --git a/src/bar.ts b/src/bar.ts
219
+ index abc123..def456 100644
220
+ --- a/src/bar.ts
221
+ +++ b/src/bar.ts
222
+ @@ -5,2 +5,4 @@ header
223
+ +line1
224
+ +line2
225
+ @@ -20,1 +22,3 @@ other
226
+ +more
227
+ +lines
228
+ `
229
+ const result = parseDiff(diffOutput)
230
+ expect(result.size).toBe(1)
231
+ const file = result.get('src/bar.ts')!
232
+ expect(file.hunks).toHaveLength(2)
233
+ expect(file.hunks[0]).toMatchInlineSnapshot(`
234
+ {
235
+ "newCount": 4,
236
+ "newStart": 5,
237
+ "oldCount": 2,
238
+ "oldStart": 5,
239
+ }
240
+ `)
241
+ expect(file.hunks[1]).toMatchInlineSnapshot(`
242
+ {
243
+ "newCount": 3,
244
+ "newStart": 22,
245
+ "oldCount": 1,
246
+ "oldStart": 20,
247
+ }
248
+ `)
249
+ })
250
+
251
+ test('parses multiple files', () => {
252
+ const diffOutput = `diff --git a/src/a.ts b/src/a.ts
253
+ index abc..def 100644
254
+ --- a/src/a.ts
255
+ +++ b/src/a.ts
256
+ @@ -1,1 +1,2 @@
257
+ +added line
258
+ diff --git a/src/b.ts b/src/b.ts
259
+ index 123..456 100644
260
+ --- a/src/b.ts
261
+ +++ b/src/b.ts
262
+ @@ -10,2 +10,1 @@
263
+ -removed line
264
+ `
265
+ const result = parseDiff(diffOutput)
266
+ expect(result.size).toBe(2)
267
+ expect(result.has('src/a.ts')).toBe(true)
268
+ expect(result.has('src/b.ts')).toBe(true)
269
+ })
270
+
271
+ test('parses hunk with single line (no count)', () => {
272
+ const diffOutput = `diff --git a/src/x.ts b/src/x.ts
273
+ --- a/src/x.ts
274
+ +++ b/src/x.ts
275
+ @@ -5 +5,2 @@
276
+ +new line
277
+ `
278
+ const result = parseDiff(diffOutput)
279
+ const file = result.get('src/x.ts')!
280
+ expect(file.hunks[0]).toMatchInlineSnapshot(`
281
+ {
282
+ "newCount": 2,
283
+ "newStart": 5,
284
+ "oldCount": 1,
285
+ "oldStart": 5,
286
+ }
287
+ `)
288
+ })
289
+
290
+ test('handles empty diff', () => {
291
+ const result = parseDiff('')
292
+ expect(result.size).toBe(0)
293
+ })
294
+
295
+ test('handles whitespace-only diff', () => {
296
+ const result = parseDiff(' \n\n ')
297
+ expect(result.size).toBe(0)
298
+ })
299
+
300
+ test('skips binary files', () => {
301
+ const diffOutput = `diff --git a/image.png b/image.png
302
+ Binary files a/image.png and b/image.png differ
303
+ diff --git a/src/code.ts b/src/code.ts
304
+ --- a/src/code.ts
305
+ +++ b/src/code.ts
306
+ @@ -1,1 +1,2 @@
307
+ +new line
308
+ `
309
+ const result = parseDiff(diffOutput)
310
+ expect(result.size).toBe(1)
311
+ expect(result.has('image.png')).toBe(false)
312
+ expect(result.has('src/code.ts')).toBe(true)
313
+ })
314
+
315
+ test('normalizes Windows paths', () => {
316
+ const diffOutput = `diff --git a/src\\path\\file.ts b/src\\path\\file.ts
317
+ --- a/src\\path\\file.ts
318
+ +++ b/src\\path\\file.ts
319
+ @@ -1,1 +1,2 @@
320
+ +new line
321
+ `
322
+ const result = parseDiff(diffOutput)
323
+ expect(result.has('src/path/file.ts')).toBe(true)
324
+ })
325
+
326
+ test('skips files with no hunks', () => {
327
+ const diffOutput = `diff --git a/src/empty.ts b/src/empty.ts
328
+ index abc..def 100644
329
+ diff --git a/src/real.ts b/src/real.ts
330
+ --- a/src/real.ts
331
+ +++ b/src/real.ts
332
+ @@ -1,1 +1,2 @@
333
+ +content
334
+ `
335
+ const result = parseDiff(diffOutput)
336
+ expect(result.size).toBe(1)
337
+ expect(result.has('src/empty.ts')).toBe(false)
338
+ expect(result.has('src/real.ts')).toBe(true)
339
+ })
340
+ })
341
+
342
+ // ============================================================================
343
+ // calculateDefinitionDiff - Determine if definition is added/updated
344
+ // ============================================================================
345
+
346
+ describe('calculateDefinitionDiff', () => {
347
+ function makeDef(line: number, endLine: number): Definition {
348
+ return {
349
+ name: 'test',
350
+ line,
351
+ endLine,
352
+ type: 'function',
353
+ exported: false,
354
+ }
355
+ }
356
+
357
+ test('returns null for definition with no changes', () => {
358
+ const def = makeDef(10, 20)
359
+ const hunks: DiffHunk[] = [
360
+ { oldStart: 1, oldCount: 2, newStart: 1, newCount: 3 }, // changes lines 1-3
361
+ ]
362
+ const result = calculateDefinitionDiff(def, hunks)
363
+ expect(result).toBeNull()
364
+ })
365
+
366
+ test('detects fully added definition', () => {
367
+ const def = makeDef(10, 15) // 6 lines
368
+ const hunks: DiffHunk[] = [
369
+ { oldStart: 5, oldCount: 0, newStart: 10, newCount: 6 }, // adds lines 10-15
370
+ ]
371
+ const result = calculateDefinitionDiff(def, hunks)
372
+ expect(result).toMatchInlineSnapshot(`
373
+ {
374
+ "added": 6,
375
+ "deleted": 0,
376
+ "status": "added",
377
+ }
378
+ `)
379
+ })
380
+
381
+ test('detects updated definition (partial overlap)', () => {
382
+ const def = makeDef(10, 20) // 11 lines
383
+ const hunks: DiffHunk[] = [
384
+ { oldStart: 12, oldCount: 2, newStart: 12, newCount: 4 }, // changes lines 12-15
385
+ ]
386
+ const result = calculateDefinitionDiff(def, hunks)
387
+ expect(result).toMatchInlineSnapshot(`
388
+ {
389
+ "added": 4,
390
+ "deleted": 2,
391
+ "status": "updated",
392
+ }
393
+ `)
394
+ })
395
+
396
+ test('handles multiple hunks in definition range', () => {
397
+ const def = makeDef(10, 30) // 21 lines
398
+ const hunks: DiffHunk[] = [
399
+ { oldStart: 12, oldCount: 1, newStart: 12, newCount: 2 }, // +1 line
400
+ { oldStart: 20, oldCount: 3, newStart: 21, newCount: 5 }, // +2 lines
401
+ ]
402
+ const result = calculateDefinitionDiff(def, hunks)
403
+ expect(result?.status).toBe('updated')
404
+ expect(result?.added).toBe(7) // 2 + 5
405
+ })
406
+
407
+ test('handles definition at exact hunk boundary', () => {
408
+ const def = makeDef(10, 12) // 3 lines
409
+ const hunks: DiffHunk[] = [
410
+ { oldStart: 8, oldCount: 0, newStart: 10, newCount: 3 }, // adds exactly lines 10-12
411
+ ]
412
+ const result = calculateDefinitionDiff(def, hunks)
413
+ expect(result?.status).toBe('added')
414
+ })
415
+
416
+ test('handles hunk that extends beyond definition', () => {
417
+ const def = makeDef(15, 20) // 6 lines
418
+ const hunks: DiffHunk[] = [
419
+ { oldStart: 10, oldCount: 5, newStart: 10, newCount: 20 }, // changes lines 10-29
420
+ ]
421
+ const result = calculateDefinitionDiff(def, hunks)
422
+ // Definition lines 15-20 overlap with hunk's new lines 10-29
423
+ expect(result?.status).toBe('updated')
424
+ expect(result?.added).toBe(6) // all 6 lines of def are in the added range
425
+ })
426
+
427
+ test('handles empty hunks array', () => {
428
+ const def = makeDef(10, 20)
429
+ const result = calculateDefinitionDiff(def, [])
430
+ expect(result).toBeNull()
431
+ })
432
+ })
433
+
434
+ // ============================================================================
435
+ // calculateFileDiff - Sum hunks for file-level stats (legacy)
436
+ // ============================================================================
437
+
438
+ describe('calculateFileDiff', () => {
439
+ test('returns null for empty hunks', () => {
440
+ const result = calculateFileDiff([])
441
+ expect(result).toBeNull()
442
+ })
443
+
444
+ test('sums single hunk correctly', () => {
445
+ const hunks: DiffHunk[] = [
446
+ { oldStart: 10, oldCount: 3, newStart: 10, newCount: 5 },
447
+ ]
448
+ const result = calculateFileDiff(hunks)
449
+ expect(result).toMatchInlineSnapshot(`
450
+ {
451
+ "added": 5,
452
+ "deleted": 3,
453
+ }
454
+ `)
455
+ })
456
+
457
+ test('sums multiple hunks correctly', () => {
458
+ const hunks: DiffHunk[] = [
459
+ { oldStart: 5, oldCount: 2, newStart: 5, newCount: 4 }, // +4-2
460
+ { oldStart: 20, oldCount: 5, newStart: 22, newCount: 3 }, // +3-5
461
+ { oldStart: 40, oldCount: 0, newStart: 40, newCount: 10 }, // +10-0
462
+ ]
463
+ const result = calculateFileDiff(hunks)
464
+ expect(result).toMatchInlineSnapshot(`
465
+ {
466
+ "added": 17,
467
+ "deleted": 7,
468
+ }
469
+ `)
470
+ })
471
+
472
+ test('handles additions only', () => {
473
+ const hunks: DiffHunk[] = [
474
+ { oldStart: 10, oldCount: 0, newStart: 10, newCount: 5 },
475
+ { oldStart: 20, oldCount: 0, newStart: 25, newCount: 3 },
476
+ ]
477
+ const result = calculateFileDiff(hunks)
478
+ expect(result).toMatchInlineSnapshot(`
479
+ {
480
+ "added": 8,
481
+ "deleted": 0,
482
+ }
483
+ `)
484
+ })
485
+
486
+ test('handles deletions only', () => {
487
+ const hunks: DiffHunk[] = [
488
+ { oldStart: 10, oldCount: 5, newStart: 10, newCount: 0 },
489
+ { oldStart: 20, oldCount: 3, newStart: 15, newCount: 0 },
490
+ ]
491
+ const result = calculateFileDiff(hunks)
492
+ expect(result).toMatchInlineSnapshot(`
493
+ {
494
+ "added": 0,
495
+ "deleted": 8,
496
+ }
497
+ `)
498
+ })
499
+
500
+ test('returns null when both added and deleted are zero', () => {
501
+ const hunks: DiffHunk[] = [
502
+ { oldStart: 10, oldCount: 0, newStart: 10, newCount: 0 },
503
+ ]
504
+ const result = calculateFileDiff(hunks)
505
+ expect(result).toBeNull()
506
+ })
507
+ })