agentmap 0.8.0 → 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 (83) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +24 -0
  3. package/dist/cli.js +37 -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 +7 -2
  10. package/dist/extract/git-status.d.ts.map +1 -1
  11. package/dist/extract/git-status.js +12 -18
  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/index.d.ts +4 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/logger.d.ts +10 -0
  24. package/dist/logger.d.ts.map +1 -0
  25. package/dist/logger.js +41 -0
  26. package/dist/logger.js.map +1 -0
  27. package/dist/map/builder.d.ts.map +1 -1
  28. package/dist/map/builder.js +23 -12
  29. package/dist/map/builder.js.map +1 -1
  30. package/dist/map/builder.test.d.ts +2 -0
  31. package/dist/map/builder.test.d.ts.map +1 -0
  32. package/dist/map/builder.test.js +66 -0
  33. package/dist/map/builder.test.js.map +1 -0
  34. package/dist/map/truncate.d.ts +7 -3
  35. package/dist/map/truncate.d.ts.map +1 -1
  36. package/dist/map/truncate.js +80 -11
  37. package/dist/map/truncate.js.map +1 -1
  38. package/dist/scanner.d.ts.map +1 -1
  39. package/dist/scanner.js +164 -65
  40. package/dist/scanner.js.map +1 -1
  41. package/dist/scanner.test.d.ts +2 -0
  42. package/dist/scanner.test.d.ts.map +1 -0
  43. package/dist/scanner.test.js +84 -0
  44. package/dist/scanner.test.js.map +1 -0
  45. package/dist/test-helpers/git-test-helpers.d.ts +13 -0
  46. package/dist/test-helpers/git-test-helpers.d.ts.map +1 -0
  47. package/dist/test-helpers/git-test-helpers.js +48 -0
  48. package/dist/test-helpers/git-test-helpers.js.map +1 -0
  49. package/dist/types.d.ts +15 -1
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +15 -3
  52. package/src/cli.ts +164 -0
  53. package/src/extract/definitions.test.ts +2040 -0
  54. package/src/extract/definitions.ts +379 -0
  55. package/src/extract/git-status.test.ts +507 -0
  56. package/src/extract/git-status.ts +359 -0
  57. package/src/extract/markdown.test.ts +159 -0
  58. package/src/extract/markdown.ts +202 -0
  59. package/src/extract/marker.test.ts +566 -0
  60. package/src/extract/marker.ts +398 -0
  61. package/src/extract/submodules.test.ts +95 -0
  62. package/src/extract/submodules.ts +269 -0
  63. package/src/extract/utils.ts +27 -0
  64. package/src/index.ts +106 -0
  65. package/src/languages/cpp.ts +129 -0
  66. package/src/languages/go.ts +72 -0
  67. package/src/languages/index.ts +231 -0
  68. package/src/languages/javascript.ts +33 -0
  69. package/src/languages/python.ts +41 -0
  70. package/src/languages/rust.ts +72 -0
  71. package/src/languages/typescript.ts +74 -0
  72. package/src/languages/zig.ts +106 -0
  73. package/src/logger.ts +55 -0
  74. package/src/map/builder.test.ts +72 -0
  75. package/src/map/builder.ts +175 -0
  76. package/src/map/truncate.ts +188 -0
  77. package/src/map/yaml.ts +66 -0
  78. package/src/parser/index.ts +53 -0
  79. package/src/parser/languages.ts +64 -0
  80. package/src/scanner.test.ts +95 -0
  81. package/src/scanner.ts +364 -0
  82. package/src/test-helpers/git-test-helpers.ts +62 -0
  83. package/src/types.ts +191 -0
package/src/scanner.ts ADDED
@@ -0,0 +1,364 @@
1
+ // Scan directory for files with header comments/docstrings and recurse into submodules.
2
+
3
+ import { execSync } from 'child_process'
4
+ import { realpathSync } from 'fs'
5
+ import pLimit from 'p-limit'
6
+ import picomatch from 'picomatch'
7
+ import { readFile } from 'fs/promises'
8
+ import { join, normalize } from 'path'
9
+ import { extractMarkerFromCode, extractMarkdownDescription } from './extract/marker.js'
10
+ import { extractDefinitions } from './extract/definitions.js'
11
+ import { getAllDiffData, applyDiffToDefinitions } from './extract/git-status.js'
12
+ import { getSubmodules, getSubmodulePaths } from './extract/submodules.js'
13
+ import { createConsoleLogger } from './logger.js'
14
+ import { parseCode, detectLanguage, LANGUAGE_EXTENSIONS } from './parser/index.js'
15
+ import type { FileResult, GenerateOptions, FileDiff, FileDiffStats, SubmoduleInfo } from './types.js'
16
+ import type { Logger } from './logger.js'
17
+
18
+ /**
19
+ * Maximum number of files to process (safety limit)
20
+ * If exceeded, returns empty results to avoid scanning huge directories
21
+ */
22
+ const MAX_FILES = 5_000_000
23
+
24
+ /**
25
+ * Supported file extensions (from LANGUAGE_EXTENSIONS)
26
+ */
27
+ const SUPPORTED_EXTENSIONS = new Set(Object.keys(LANGUAGE_EXTENSIONS))
28
+
29
+ /**
30
+ * Check if a file has a supported extension
31
+ */
32
+ function isSupportedFile(filepath: string): boolean {
33
+ const ext = filepath.slice(filepath.lastIndexOf('.'))
34
+ return SUPPORTED_EXTENSIONS.has(ext)
35
+ }
36
+
37
+ /**
38
+ * Check if a file is a README file (case-insensitive, with or without .md extension)
39
+ */
40
+ function isReadmeFile(filepath: string): boolean {
41
+ const filename = filepath.split(/[/\\]/).pop()?.toLowerCase() ?? ''
42
+ return filename === 'readme.md' || filename === 'readme'
43
+ }
44
+
45
+ /**
46
+ * A tracked file with its blob SHA (for dedup) and normalized path.
47
+ */
48
+ interface GitFileEntry {
49
+ path: string
50
+ sha: string
51
+ }
52
+
53
+ /**
54
+ * Get tracked files using git ls-files -z -s.
55
+ * NUL-delimited for safe cross-platform parsing of any filename.
56
+ * Filters out symlinks (mode 120000).
57
+ * Returns path + blob SHA for duplicate detection.
58
+ *
59
+ * Format per entry: "<mode> <sha> <stage>\t<path>\0"
60
+ */
61
+ function getGitFiles(dir: string): GitFileEntry[] {
62
+ const maxBuffer = 1024 * 10000000
63
+ try {
64
+ const stdout = execSync('git ls-files -z -s', {
65
+ cwd: dir,
66
+ maxBuffer,
67
+ encoding: 'utf8',
68
+ })
69
+
70
+ const results: GitFileEntry[] = []
71
+
72
+ // Split on NUL byte, filter empty trailing entry
73
+ const entries = stdout.split('\0').filter(Boolean)
74
+ for (const entry of entries) {
75
+ // Format: "<mode> <sha> <stage>\t<path>"
76
+ const tabIdx = entry.indexOf('\t')
77
+ if (tabIdx === -1) continue
78
+
79
+ const meta = entry.slice(0, tabIdx)
80
+ const path = entry.slice(tabIdx + 1)
81
+
82
+ const spaceIdx = meta.indexOf(' ')
83
+ if (spaceIdx === -1) continue
84
+
85
+ const mode = meta.slice(0, spaceIdx)
86
+ const sha = meta.slice(spaceIdx + 1, meta.indexOf(' ', spaceIdx + 1))
87
+
88
+ // Skip symlinks (mode 120000)
89
+ if (mode === '120000') continue
90
+
91
+ results.push({ path: normalize(path), sha })
92
+ }
93
+
94
+ return results
95
+ } catch {
96
+ return []
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Build a map of blob SHA → shortest path for duplicate detection.
102
+ * Files sharing the same blob SHA are exact duplicates in git.
103
+ */
104
+ function buildDuplicateMap(files: GitFileEntry[], pathPrefix: string): Map<string, string> {
105
+ // Group paths by SHA
106
+ const shaToEntries = new Map<string, string[]>()
107
+ for (const { path, sha } of files) {
108
+ const prefixed = joinRelativePath(pathPrefix, path)
109
+ const existing = shaToEntries.get(sha)
110
+ if (existing) {
111
+ existing.push(prefixed)
112
+ } else {
113
+ shaToEntries.set(sha, [prefixed])
114
+ }
115
+ }
116
+
117
+ // For each group with >1 file, shortest path is the original
118
+ const duplicateOf = new Map<string, string>()
119
+ for (const paths of shaToEntries.values()) {
120
+ if (paths.length < 2) continue
121
+ paths.sort((a, b) => a.length - b.length || a.localeCompare(b))
122
+ const original = paths[0]
123
+ for (let i = 1; i < paths.length; i++) {
124
+ duplicateOf.set(paths[i], original)
125
+ }
126
+ }
127
+
128
+ return duplicateOf
129
+ }
130
+
131
+
132
+
133
+ /**
134
+ * Result of scanning a directory, including both file results and submodule info
135
+ */
136
+ export interface ScanResult {
137
+ files: FileResult[]
138
+ submodules: SubmoduleInfo[]
139
+ }
140
+
141
+ interface ScanRepoOptions {
142
+ repoDir: string
143
+ pathPrefix: string
144
+ includeDiff: boolean
145
+ includeSubmodules: boolean
146
+ logger: Logger
147
+ isIncluded?: (path: string) => boolean
148
+ isIgnored?: (path: string) => boolean
149
+ visitedRepoDirs: Set<string>
150
+ }
151
+
152
+ function normalizeRelativePath(path: string): string {
153
+ return path.replace(/\\/g, '/')
154
+ }
155
+
156
+ function joinRelativePath(prefix: string, path: string): string {
157
+ const normalizedPath = normalizeRelativePath(path)
158
+ return prefix ? `${prefix}/${normalizedPath}` : normalizedPath
159
+ }
160
+
161
+ function getCanonicalRepoDir(dir: string): string {
162
+ try {
163
+ return realpathSync(dir)
164
+ } catch {
165
+ return dir
166
+ }
167
+ }
168
+
169
+ async function scanRepo(options: ScanRepoOptions): Promise<ScanResult> {
170
+ const canonicalRepoDir = getCanonicalRepoDir(options.repoDir)
171
+ if (options.visitedRepoDirs.has(canonicalRepoDir)) {
172
+ return { files: [], submodules: [] }
173
+ }
174
+ options.visitedRepoDirs.add(canonicalRepoDir)
175
+
176
+ let submodules: SubmoduleInfo[] = []
177
+ const directSubmodules = options.includeSubmodules ? getSubmodules(options.repoDir) : []
178
+ const directSubmodulePathSet = options.includeSubmodules
179
+ ? new Set(directSubmodules.map(submodule => submodule.path))
180
+ : getSubmodulePaths(options.repoDir)
181
+
182
+ if (options.includeSubmodules) {
183
+ submodules = directSubmodules.map(submodule => ({
184
+ ...submodule,
185
+ path: joinRelativePath(options.pathPrefix, submodule.path),
186
+ }))
187
+ }
188
+
189
+ const normalizedSubmodulePaths = new Set<string>()
190
+ for (const path of directSubmodulePathSet) {
191
+ normalizedSubmodulePaths.add(normalize(path))
192
+ }
193
+
194
+ const allGitFiles = getGitFiles(options.repoDir)
195
+
196
+ // Build duplicate map before filtering (needs all files to detect dupes)
197
+ const duplicateOf = buildDuplicateMap(allGitFiles, options.pathPrefix)
198
+
199
+ let gitFiles = allGitFiles.filter(f => !normalizedSubmodulePaths.has(f.path))
200
+ gitFiles = gitFiles.filter(f => isSupportedFile(f.path) || isReadmeFile(f.path))
201
+
202
+ gitFiles = gitFiles.filter(f => {
203
+ const relativePath = joinRelativePath(options.pathPrefix, f.path)
204
+
205
+ if (options.isIncluded && !options.isIncluded(relativePath)) {
206
+ return false
207
+ }
208
+
209
+ if (options.isIgnored && options.isIgnored(relativePath)) {
210
+ return false
211
+ }
212
+
213
+ return true
214
+ })
215
+
216
+ if (gitFiles.length > MAX_FILES) {
217
+ options.logger?.warn(`Warning: Too many files (${gitFiles.length} > ${MAX_FILES}), skipping scan`)
218
+ return { files: [], submodules }
219
+ }
220
+
221
+ let fileStats: Map<string, FileDiffStats> | null = null
222
+ let fileDiffs: Map<string, FileDiff> | null = null
223
+
224
+ if (options.includeDiff) {
225
+ try {
226
+ const diffData = getAllDiffData(options.repoDir, directSubmodulePathSet, options.logger)
227
+ fileStats = diffData.fileStats
228
+ fileDiffs = diffData.fileDiffs
229
+ } catch {
230
+ fileStats = null
231
+ fileDiffs = null
232
+ }
233
+ }
234
+
235
+ const limit = pLimit(20)
236
+ const resultPromises = gitFiles.map(({ path: relativePath }) => {
237
+ const fullPath = join(options.repoDir, relativePath)
238
+ const normalizedPath = normalizeRelativePath(relativePath)
239
+ const prefixedRelativePath = joinRelativePath(options.pathPrefix, relativePath)
240
+ const fileDiff = fileDiffs?.get(normalizedPath)
241
+ const stats = fileStats?.get(normalizedPath)
242
+ const dupOriginal = duplicateOf.get(prefixedRelativePath)
243
+
244
+ return limit(async () => {
245
+ try {
246
+ // If this file is a duplicate, return a stub pointing to the original
247
+ if (dupOriginal) {
248
+ return {
249
+ relativePath: prefixedRelativePath,
250
+ duplicateOf: dupOriginal,
251
+ definitions: [],
252
+ } satisfies FileResult
253
+ }
254
+ return await processFile(fullPath, prefixedRelativePath, fileDiff, stats)
255
+ } catch {
256
+ return null
257
+ }
258
+ })
259
+ })
260
+
261
+ const nestedResults = options.includeSubmodules
262
+ ? await Promise.all(directSubmodules.map(async submodule => {
263
+ if (!submodule.initialized) {
264
+ return { files: [], submodules: [] }
265
+ }
266
+
267
+ return scanRepo({
268
+ ...options,
269
+ repoDir: join(options.repoDir, submodule.path),
270
+ pathPrefix: joinRelativePath(options.pathPrefix, submodule.path),
271
+ })
272
+ }))
273
+ : []
274
+
275
+ const results = await Promise.all(resultPromises)
276
+
277
+ for (const nested of nestedResults) {
278
+ submodules.push(...nested.submodules)
279
+ }
280
+
281
+ return {
282
+ files: [
283
+ ...results.filter((result): result is FileResult => result !== null),
284
+ ...nestedResults.flatMap(result => result.files),
285
+ ],
286
+ submodules,
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Scan directory and process files with header comments
292
+ */
293
+ export async function scanDirectory(options: GenerateOptions = {}): Promise<ScanResult> {
294
+ const dir = options.dir ?? process.cwd()
295
+ const logger = options.logger ?? createConsoleLogger()
296
+ const ignorePatterns = (options.ignore ?? []).filter((p): p is string => !!p)
297
+ const filterPatterns = (options.filter ?? []).filter((p): p is string => !!p)
298
+ return scanRepo({
299
+ repoDir: dir,
300
+ pathPrefix: '',
301
+ includeDiff: options.diff ?? false,
302
+ includeSubmodules: options.submodules !== false,
303
+ logger,
304
+ isIncluded: filterPatterns.length > 0 ? picomatch(filterPatterns) : undefined,
305
+ isIgnored: ignorePatterns.length > 0 ? picomatch(ignorePatterns) : undefined,
306
+ visitedRepoDirs: new Set(),
307
+ })
308
+ }
309
+
310
+ /**
311
+ * Process a single file - check for marker and extract definitions
312
+ */
313
+ async function processFile(
314
+ fullPath: string,
315
+ relativePath: string,
316
+ fileDiff?: FileDiff,
317
+ fileStats?: FileDiffStats
318
+ ): Promise<FileResult | null> {
319
+ // Handle README.md files specially
320
+ if (isReadmeFile(relativePath)) {
321
+ const description = await extractMarkdownDescription(fullPath)
322
+ if (!description) {
323
+ return null
324
+ }
325
+ return {
326
+ relativePath,
327
+ description,
328
+ definitions: [],
329
+ diff: fileStats,
330
+ }
331
+ }
332
+
333
+ // Detect language first
334
+ const language = detectLanguage(relativePath)
335
+ if (!language) {
336
+ return null
337
+ }
338
+
339
+ // Read file once for both marker extraction and definition parsing
340
+ const code = await readFile(fullPath, 'utf8')
341
+
342
+ // Check for marker using the code we already read
343
+ const marker = await extractMarkerFromCode(code, language)
344
+ if (!marker.found) {
345
+ return null
346
+ }
347
+
348
+ // Parse and extract definitions using the same code
349
+ const tree = await parseCode(code, language)
350
+ let definitions = extractDefinitions(tree.rootNode, language)
351
+
352
+ // Apply diff info if available (for definition-level stats)
353
+ if (fileDiff) {
354
+ definitions = applyDiffToDefinitions(definitions, fileDiff)
355
+ }
356
+
357
+ return {
358
+ relativePath,
359
+ description: marker.description,
360
+ definitions,
361
+ // Use pre-calculated file stats from --numstat (more reliable)
362
+ diff: fileStats,
363
+ }
364
+ }
@@ -0,0 +1,62 @@
1
+ // Helpers for creating temporary git repositories in tests.
2
+
3
+ import { execSync } from 'child_process'
4
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'fs'
5
+ import { tmpdir } from 'os'
6
+ import { join } from 'path'
7
+
8
+ export interface TempRepo {
9
+ dir: string
10
+ }
11
+
12
+ function run(command: string, cwd: string): string {
13
+ return execSync(command, {
14
+ cwd,
15
+ encoding: 'utf8',
16
+ stdio: ['pipe', 'pipe', 'pipe'],
17
+ })
18
+ }
19
+
20
+ export function createTempDir(prefix: string): string {
21
+ return mkdtempSync(join(tmpdir(), prefix))
22
+ }
23
+
24
+ export function initRepo(dir: string): TempRepo {
25
+ run('git init', dir)
26
+ run('git config user.name "agentmap tests"', dir)
27
+ run('git config user.email "agentmap@example.com"', dir)
28
+ return { dir }
29
+ }
30
+
31
+ export function createRepo(prefix: string): TempRepo {
32
+ const dir = createTempDir(prefix)
33
+ mkdirSync(dir, { recursive: true })
34
+ return initRepo(dir)
35
+ }
36
+
37
+ export function writeTrackedFile(repo: TempRepo, relativePath: string, content: string): void {
38
+ const filePath = join(repo.dir, relativePath)
39
+ mkdirSync(join(filePath, '..'), { recursive: true })
40
+ writeFileSync(filePath, content, 'utf8')
41
+ }
42
+
43
+ export function commitAll(repo: TempRepo, message: string): void {
44
+ run('git add .', repo.dir)
45
+ run(`git commit -m ${JSON.stringify(message)}`, repo.dir)
46
+ }
47
+
48
+ export function shortHead(repo: TempRepo): string {
49
+ return run('git rev-parse --short HEAD', repo.dir).trim()
50
+ }
51
+
52
+ export function addSubmodule(repo: TempRepo, sourceDir: string, targetPath: string): void {
53
+ run(`git -c protocol.file.allow=always submodule add ${JSON.stringify(sourceDir)} ${JSON.stringify(targetPath)}`, repo.dir)
54
+ }
55
+
56
+ export function updateSubmodulesRecursive(repo: TempRepo): void {
57
+ run('git -c protocol.file.allow=always submodule update --init --recursive', repo.dir)
58
+ }
59
+
60
+ export function deinitSubmodule(repo: TempRepo, targetPath: string): void {
61
+ run(`git submodule deinit -f ${JSON.stringify(targetPath)}`, repo.dir)
62
+ }
package/src/types.ts ADDED
@@ -0,0 +1,191 @@
1
+ // Core type definitions for the codebase map.
2
+
3
+ import type Parser from 'web-tree-sitter'
4
+ import type { Logger } from './logger.js'
5
+
6
+ /**
7
+ * Supported programming languages
8
+ */
9
+ export type Language =
10
+ | 'typescript'
11
+ | 'javascript'
12
+ | 'python'
13
+ | 'rust'
14
+ | 'go'
15
+ | 'zig'
16
+ | 'cpp'
17
+
18
+ /**
19
+ * Symbol definitions mapping: name -> description string
20
+ */
21
+ export interface DefEntry {
22
+ [symbolName: string]: string
23
+ }
24
+
25
+ /**
26
+ * Git diff stats for a file (total lines added/deleted)
27
+ */
28
+ export interface FileDiffStats {
29
+ added: number
30
+ deleted: number
31
+ }
32
+
33
+ /**
34
+ * A file entry in the map
35
+ */
36
+ export interface FileEntry {
37
+ description?: string
38
+ diff?: string // formatted as "+N-M" or "+N" or "-M"
39
+ defs?: DefEntry
40
+ exports?: DefEntry // used instead of defs when truncating files with exported symbols
41
+ }
42
+
43
+ /**
44
+ * A submodule entry in the map
45
+ */
46
+ export interface SubmoduleEntry {
47
+ submodule: string // "branch @ sha" or "detached @ sha"
48
+ dirty?: string // "modified" if submodule has uncommitted changes
49
+ }
50
+
51
+ /**
52
+ * A submodule node in the map.
53
+ * Carries submodule metadata and can also contain nested files/directories.
54
+ */
55
+ export interface SubmoduleNode extends SubmoduleEntry {
56
+ [name: string]: MapNode | FileEntry | SubmoduleNode | string | undefined
57
+ }
58
+
59
+ /**
60
+ * Info about a git submodule discovered in the repo
61
+ */
62
+ export interface SubmoduleInfo {
63
+ /** Relative path of the submodule in the parent repo */
64
+ path: string
65
+ /** Current HEAD commit SHA (short) */
66
+ commit: string
67
+ /** Checked-out branch name, or undefined if detached HEAD */
68
+ branch?: string
69
+ /** Remote URL from .gitmodules */
70
+ url?: string
71
+ /** Whether the submodule has uncommitted changes */
72
+ dirty?: boolean
73
+ /** Whether the submodule is initialized */
74
+ initialized: boolean
75
+ }
76
+
77
+ /**
78
+ * Recursive map node - either a directory (with children), a file entry, or a submodule entry
79
+ */
80
+ export interface MapNode {
81
+ [name: string]: MapNode | FileEntry | SubmoduleNode
82
+ }
83
+
84
+ /**
85
+ * Result of extracting marker and description from a file
86
+ */
87
+ export interface MarkerResult {
88
+ found: boolean
89
+ description?: string
90
+ }
91
+
92
+ /**
93
+ * Types of definitions we extract
94
+ */
95
+ export type DefinitionType =
96
+ | 'function'
97
+ | 'class'
98
+ | 'struct'
99
+ | 'union'
100
+ | 'trait'
101
+ | 'type'
102
+ | 'interface'
103
+ | 'const'
104
+ | 'enum'
105
+
106
+ /**
107
+ * Git status for a definition
108
+ */
109
+ export type DefinitionStatus = 'added' | 'updated'
110
+
111
+ /**
112
+ * Git diff stats for a definition
113
+ */
114
+ export interface DefinitionDiff {
115
+ status: DefinitionStatus
116
+ added: number // lines added
117
+ deleted: number // lines deleted
118
+ }
119
+
120
+ /**
121
+ * A definition extracted from source code
122
+ */
123
+ export interface Definition {
124
+ name: string
125
+ line: number // 1-based start line
126
+ endLine: number // 1-based end line
127
+ type: DefinitionType
128
+ exported: boolean
129
+ extern?: boolean // true for extern declarations (C/C++/Zig)
130
+ diff?: DefinitionDiff // only present when --diff flag used
131
+ }
132
+
133
+ /**
134
+ * Result of processing a single file
135
+ */
136
+ export interface FileResult {
137
+ relativePath: string
138
+ description?: string
139
+ definitions: Definition[]
140
+ diff?: FileDiffStats // only present when --diff flag used
141
+ /** If set, this file is an exact duplicate of another file (by git blob SHA) */
142
+ duplicateOf?: string
143
+ }
144
+
145
+ /**
146
+ * Options for generating the map
147
+ */
148
+ export interface GenerateOptions {
149
+ /** Directory to scan (default: cwd) */
150
+ dir?: string
151
+ /** Glob patterns to ignore */
152
+ ignore?: string[]
153
+ /** Glob patterns to filter - only include matching files */
154
+ filter?: string[]
155
+ /** Include git diff status for definitions */
156
+ diff?: boolean
157
+ /** Git ref to diff against (default: HEAD for unstaged, --cached for staged) */
158
+ diffBase?: string
159
+ /** Max definitions per file before truncation (default: 25) */
160
+ maxDefs?: number
161
+ /** Max characters for file descriptions before truncation (default: 300). Rounds up to full line. */
162
+ maxDescChars?: number
163
+ /** Include submodule info in the map (default: true) */
164
+ submodules?: boolean
165
+ /** Logger implementation (default: console logger) */
166
+ logger?: Logger
167
+ }
168
+
169
+ /**
170
+ * A hunk from git diff output
171
+ */
172
+ export interface DiffHunk {
173
+ oldStart: number
174
+ oldCount: number
175
+ newStart: number
176
+ newCount: number
177
+ }
178
+
179
+ /**
180
+ * Parsed diff for a single file
181
+ */
182
+ export interface FileDiff {
183
+ path: string
184
+ hunks: DiffHunk[]
185
+ }
186
+
187
+ /**
188
+ * Re-export parser types
189
+ */
190
+ export type SyntaxNode = Parser.SyntaxNode
191
+ export type SyntaxTree = Parser.Tree