@blamechris/repo-memory 0.8.0 → 0.10.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 (49) hide show
  1. package/README.md +19 -5
  2. package/dist/cache/store.d.ts +10 -0
  3. package/dist/cache/store.d.ts.map +1 -1
  4. package/dist/cache/store.js +21 -0
  5. package/dist/cache/store.js.map +1 -1
  6. package/dist/cli/index-command.d.ts +30 -0
  7. package/dist/cli/index-command.d.ts.map +1 -0
  8. package/dist/cli/index-command.js +110 -0
  9. package/dist/cli/index-command.js.map +1 -0
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +8 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/grammars/tree-sitter-go.wasm +0 -0
  15. package/dist/grammars/tree-sitter-javascript.wasm +0 -0
  16. package/dist/grammars/tree-sitter-python.wasm +0 -0
  17. package/dist/grammars/tree-sitter-rust.wasm +0 -0
  18. package/dist/grammars/tree-sitter-tsx.wasm +0 -0
  19. package/dist/grammars/tree-sitter-typescript.wasm +0 -0
  20. package/dist/indexer/ast-summarizer.d.ts +10 -0
  21. package/dist/indexer/ast-summarizer.d.ts.map +1 -0
  22. package/dist/indexer/ast-summarizer.js +958 -0
  23. package/dist/indexer/ast-summarizer.js.map +1 -0
  24. package/dist/indexer/project-map.d.ts.map +1 -1
  25. package/dist/indexer/project-map.js +5 -3
  26. package/dist/indexer/project-map.js.map +1 -1
  27. package/dist/indexer/smart-summarizer.d.ts +2 -2
  28. package/dist/indexer/smart-summarizer.d.ts.map +1 -1
  29. package/dist/indexer/smart-summarizer.js +4 -4
  30. package/dist/indexer/smart-summarizer.js.map +1 -1
  31. package/dist/indexer/summarize.d.ts +21 -0
  32. package/dist/indexer/summarize.d.ts.map +1 -0
  33. package/dist/indexer/summarize.js +56 -0
  34. package/dist/indexer/summarize.js.map +1 -0
  35. package/dist/indexer/summarizer.js +1 -1
  36. package/dist/indexer/summarizer.js.map +1 -1
  37. package/dist/persistence/db.d.ts +2 -0
  38. package/dist/persistence/db.d.ts.map +1 -1
  39. package/dist/persistence/db.js +16 -1
  40. package/dist/persistence/db.js.map +1 -1
  41. package/dist/server.js +14 -5
  42. package/dist/server.js.map +1 -1
  43. package/dist/tools/force-reread.d.ts.map +1 -1
  44. package/dist/tools/force-reread.js +3 -2
  45. package/dist/tools/force-reread.js.map +1 -1
  46. package/dist/tools/get-file-summary.d.ts.map +1 -1
  47. package/dist/tools/get-file-summary.js +3 -2
  48. package/dist/tools/get-file-summary.js.map +1 -1
  49. package/package.json +5 -3
@@ -0,0 +1,958 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Parser, Language } from 'web-tree-sitter';
6
+ import { summarizeFile } from './summarizer.js';
7
+ const EXT_TO_GRAMMAR = {
8
+ '.ts': 'typescript',
9
+ '.mts': 'typescript',
10
+ '.cts': 'typescript',
11
+ '.d.ts': 'typescript',
12
+ '.tsx': 'tsx',
13
+ '.js': 'javascript',
14
+ '.jsx': 'javascript',
15
+ '.mjs': 'javascript',
16
+ '.cjs': 'javascript',
17
+ '.py': 'python',
18
+ '.go': 'go',
19
+ '.rs': 'rust',
20
+ };
21
+ const MAX_PURPOSE_LENGTH = 160;
22
+ const require = createRequire(import.meta.url);
23
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
24
+ /**
25
+ * Locate a grammar's .wasm file. Vendored grammars in `dist/grammars/`
26
+ * (copied by `scripts/copy-grammars.mjs` at build time) take precedence, so
27
+ * the published package works without `tree-sitter-wasms` installed. In dev
28
+ * and under vitest the code runs from `src/`, where no vendored copy exists,
29
+ * so we fall back to resolving the `tree-sitter-wasms` devDependency.
30
+ */
31
+ function resolveGrammarWasm(grammar) {
32
+ const vendored = join(moduleDir, '..', 'grammars', `tree-sitter-${grammar}.wasm`);
33
+ if (existsSync(vendored))
34
+ return vendored;
35
+ return require.resolve(`tree-sitter-wasms/out/tree-sitter-${grammar}.wasm`);
36
+ }
37
+ let initPromise = null;
38
+ let runtimeBroken = false;
39
+ const languageCache = new Map();
40
+ const parserCache = new Map();
41
+ function getExtension(filePath) {
42
+ if (filePath.endsWith('.d.ts'))
43
+ return '.d.ts';
44
+ const dot = filePath.lastIndexOf('.');
45
+ return dot === -1 ? '' : filePath.slice(dot);
46
+ }
47
+ function getBasename(filePath) {
48
+ const normalized = filePath.replace(/\\/g, '/');
49
+ const slash = normalized.lastIndexOf('/');
50
+ return slash === -1 ? normalized : normalized.slice(slash + 1);
51
+ }
52
+ async function getParser(grammar) {
53
+ if (!initPromise) {
54
+ initPromise = Parser.init();
55
+ }
56
+ await initPromise;
57
+ const cached = parserCache.get(grammar);
58
+ if (cached)
59
+ return cached;
60
+ let languagePromise = languageCache.get(grammar);
61
+ if (!languagePromise) {
62
+ languagePromise = Language.load(resolveGrammarWasm(grammar));
63
+ languageCache.set(grammar, languagePromise);
64
+ }
65
+ const language = await languagePromise;
66
+ const parser = new Parser();
67
+ parser.setLanguage(language);
68
+ parserCache.set(grammar, parser);
69
+ return parser;
70
+ }
71
+ function stripQuotes(text) {
72
+ return text.replace(/^['"`]|['"`]$/g, '');
73
+ }
74
+ function emptyResult() {
75
+ return {
76
+ exports: [],
77
+ imports: [],
78
+ topLevelDeclarations: [],
79
+ classes: [],
80
+ functions: [],
81
+ typeNames: [],
82
+ constNames: [],
83
+ fileDoc: null,
84
+ };
85
+ }
86
+ function dedupeResult(out) {
87
+ out.exports = [...new Set(out.exports)];
88
+ out.imports = [...new Set(out.imports)];
89
+ out.topLevelDeclarations = [...new Set(out.topLevelDeclarations)];
90
+ return out;
91
+ }
92
+ /** Keep only the first sentence so multi-sentence doc lines stay short. */
93
+ function firstSentence(line) {
94
+ const sentenceEnd = line.indexOf('. ');
95
+ return sentenceEnd === -1 ? line : line.slice(0, sentenceEnd + 1);
96
+ }
97
+ /** First meaningful line of a comment, with `/**`, `*` and `//` markers removed. */
98
+ function commentFirstLine(text) {
99
+ const cleaned = text
100
+ .replace(/^\/\*+/, '')
101
+ .replace(/\*+\/$/, '');
102
+ for (const rawLine of cleaned.split('\n')) {
103
+ const line = rawLine.replace(/^\s*(?:\*|\/\/)?\s*/, '').trim();
104
+ if (line.length > 0 && !line.startsWith('@') && !line.startsWith('eslint')) {
105
+ return firstSentence(line);
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+ /** JSDoc-style block comment immediately preceding `node` at the same level. */
111
+ function precedingDoc(node) {
112
+ const prev = node.previousNamedSibling;
113
+ if (prev && prev.type === 'comment' && prev.text.startsWith('/**')) {
114
+ return commentFirstLine(prev.text);
115
+ }
116
+ return null;
117
+ }
118
+ function countMethods(classNode) {
119
+ const body = classNode.childForFieldName('body');
120
+ if (!body)
121
+ return 0;
122
+ let count = 0;
123
+ for (const child of body.namedChildren) {
124
+ if (!child)
125
+ continue;
126
+ if (child.type === 'method_definition' || child.type === 'abstract_method_signature') {
127
+ const name = child.childForFieldName('name')?.text;
128
+ if (name !== 'constructor')
129
+ count++;
130
+ }
131
+ }
132
+ return count;
133
+ }
134
+ function hasDefaultKeyword(exportStatement) {
135
+ return exportStatement.children.some((c) => c !== null && c.type === 'default');
136
+ }
137
+ /** Handles one top-level declaration node; doc comes from the outermost statement. */
138
+ function collectDeclaration(node, exported, doc, out) {
139
+ switch (node.type) {
140
+ case 'lexical_declaration':
141
+ case 'variable_declaration': {
142
+ const kindNode = node.children.find((c) => c !== null && (c.type === 'const' || c.type === 'let' || c.type === 'var'));
143
+ const kind = kindNode?.text ?? 'const';
144
+ for (const declarator of node.namedChildren) {
145
+ if (!declarator || declarator.type !== 'variable_declarator')
146
+ continue;
147
+ const nameNode = declarator.childForFieldName('name');
148
+ if (!nameNode || nameNode.type !== 'identifier')
149
+ continue;
150
+ const name = nameNode.text;
151
+ out.topLevelDeclarations.push(`${kind} ${name}`);
152
+ if (kind === 'const')
153
+ out.constNames.push(name);
154
+ if (exported)
155
+ out.exports.push(name);
156
+ }
157
+ break;
158
+ }
159
+ case 'function_declaration':
160
+ case 'generator_function_declaration': {
161
+ const name = node.childForFieldName('name')?.text;
162
+ if (name) {
163
+ out.topLevelDeclarations.push(`function ${name}`);
164
+ out.functions.push({ name, doc, exported });
165
+ if (exported)
166
+ out.exports.push(name);
167
+ }
168
+ break;
169
+ }
170
+ case 'class_declaration':
171
+ case 'abstract_class_declaration': {
172
+ const name = node.childForFieldName('name')?.text;
173
+ if (name) {
174
+ out.topLevelDeclarations.push(`class ${name}`);
175
+ out.classes.push({ name, kind: 'class', methodCount: countMethods(node), doc, exported });
176
+ if (exported)
177
+ out.exports.push(name);
178
+ }
179
+ break;
180
+ }
181
+ case 'interface_declaration': {
182
+ const name = node.childForFieldName('name')?.text;
183
+ if (name) {
184
+ out.topLevelDeclarations.push(`interface ${name}`);
185
+ out.typeNames.push(name);
186
+ if (exported)
187
+ out.exports.push(name);
188
+ }
189
+ break;
190
+ }
191
+ case 'type_alias_declaration': {
192
+ const name = node.childForFieldName('name')?.text;
193
+ if (name) {
194
+ out.topLevelDeclarations.push(`type ${name}`);
195
+ out.typeNames.push(name);
196
+ if (exported)
197
+ out.exports.push(name);
198
+ }
199
+ break;
200
+ }
201
+ case 'enum_declaration': {
202
+ const name = node.childForFieldName('name')?.text;
203
+ if (name) {
204
+ out.topLevelDeclarations.push(`enum ${name}`);
205
+ out.typeNames.push(name);
206
+ if (exported)
207
+ out.exports.push(name);
208
+ }
209
+ break;
210
+ }
211
+ case 'ambient_declaration': {
212
+ // `declare const x: number;` — recurse into the inner declaration.
213
+ for (const inner of node.namedChildren) {
214
+ if (inner)
215
+ collectDeclaration(inner, exported, doc, out);
216
+ }
217
+ break;
218
+ }
219
+ default:
220
+ break;
221
+ }
222
+ }
223
+ function collectExportStatement(node, out) {
224
+ const doc = precedingDoc(node);
225
+ const source = node.childForFieldName('source');
226
+ if (source) {
227
+ // Re-export: `export { a, b as c } from './x.js'` / `export * from './x.js'`.
228
+ out.imports.push(stripQuotes(source.text));
229
+ }
230
+ const declaration = node.childForFieldName('declaration');
231
+ if (declaration) {
232
+ collectDeclaration(declaration, true, doc, out);
233
+ if (hasDefaultKeyword(node))
234
+ out.exports.push('default');
235
+ return;
236
+ }
237
+ const value = node.childForFieldName('value');
238
+ if (value) {
239
+ // `export default <expression>;`
240
+ out.exports.push('default');
241
+ return;
242
+ }
243
+ for (const child of node.namedChildren) {
244
+ if (!child)
245
+ continue;
246
+ if (child.type === 'export_clause') {
247
+ for (const spec of child.namedChildren) {
248
+ if (!spec || spec.type !== 'export_specifier')
249
+ continue;
250
+ const alias = spec.childForFieldName('alias')?.text;
251
+ const name = spec.childForFieldName('name')?.text;
252
+ const visible = alias ?? name;
253
+ if (visible)
254
+ out.exports.push(visible);
255
+ }
256
+ }
257
+ else if (child.type === 'namespace_export') {
258
+ // `export * as ns from './x.js'`
259
+ const name = child.namedChildren.find((c) => c !== null)?.text;
260
+ if (name)
261
+ out.exports.push(name);
262
+ }
263
+ }
264
+ }
265
+ function extractTsJs(root) {
266
+ const out = emptyResult();
267
+ let seenCode = false;
268
+ for (const child of root.namedChildren) {
269
+ if (!child)
270
+ continue;
271
+ switch (child.type) {
272
+ case 'comment':
273
+ if (!seenCode && out.fileDoc === null) {
274
+ out.fileDoc = commentFirstLine(child.text);
275
+ }
276
+ break;
277
+ case 'import_statement': {
278
+ seenCode = true;
279
+ const source = child.childForFieldName('source');
280
+ if (source)
281
+ out.imports.push(stripQuotes(source.text));
282
+ break;
283
+ }
284
+ case 'export_statement':
285
+ seenCode = true;
286
+ collectExportStatement(child, out);
287
+ break;
288
+ default:
289
+ seenCode = true;
290
+ collectDeclaration(child, false, precedingDoc(child), out);
291
+ break;
292
+ }
293
+ }
294
+ return dedupeResult(out);
295
+ }
296
+ // ---------------------------------------------------------------------------
297
+ // Python extraction
298
+ // ---------------------------------------------------------------------------
299
+ /** First line of a Python string literal's content, quotes and prefixes removed. */
300
+ function pyStringFirstLine(stringNode) {
301
+ const content = stringNode.namedChildren.find((c) => c?.type === 'string_content');
302
+ if (!content)
303
+ return null;
304
+ for (const rawLine of content.text.split('\n')) {
305
+ const line = rawLine.trim();
306
+ if (line.length > 0)
307
+ return firstSentence(line);
308
+ }
309
+ return null;
310
+ }
311
+ /** Docstring first line of a `block` (or `module`) node, when present. */
312
+ function pyDocstring(body) {
313
+ const first = body?.namedChildren.find((c) => c !== null && c.type !== 'comment');
314
+ if (first && first.type === 'expression_statement') {
315
+ const str = first.namedChildren.find((c) => c?.type === 'string');
316
+ if (str)
317
+ return pyStringFirstLine(str);
318
+ }
319
+ return null;
320
+ }
321
+ function countPyMethods(classNode) {
322
+ const body = classNode.childForFieldName('body');
323
+ if (!body)
324
+ return 0;
325
+ let count = 0;
326
+ for (let child of body.namedChildren) {
327
+ if (!child)
328
+ continue;
329
+ if (child.type === 'decorated_definition') {
330
+ child = child.childForFieldName('definition') ?? child;
331
+ }
332
+ if (child.type === 'function_definition') {
333
+ if (child.childForFieldName('name')?.text !== '__init__')
334
+ count++;
335
+ }
336
+ }
337
+ return count;
338
+ }
339
+ function extractPython(root) {
340
+ const out = emptyResult();
341
+ out.fileDoc = pyDocstring(root);
342
+ let allList = null;
343
+ const publicNames = [];
344
+ for (let child of root.namedChildren) {
345
+ if (!child)
346
+ continue;
347
+ if (child.type === 'decorated_definition') {
348
+ child = child.childForFieldName('definition') ?? child;
349
+ }
350
+ switch (child.type) {
351
+ case 'import_statement': {
352
+ for (const item of child.namedChildren) {
353
+ if (!item)
354
+ continue;
355
+ if (item.type === 'dotted_name')
356
+ out.imports.push(item.text);
357
+ else if (item.type === 'aliased_import') {
358
+ const name = item.childForFieldName('name');
359
+ if (name)
360
+ out.imports.push(name.text);
361
+ }
362
+ }
363
+ break;
364
+ }
365
+ case 'import_from_statement': {
366
+ const module = child.childForFieldName('module_name');
367
+ if (module)
368
+ out.imports.push(module.text);
369
+ break;
370
+ }
371
+ case 'function_definition': {
372
+ const name = child.childForFieldName('name')?.text;
373
+ if (name) {
374
+ const isPublic = !name.startsWith('_');
375
+ out.topLevelDeclarations.push(`def ${name}`);
376
+ out.functions.push({
377
+ name,
378
+ doc: pyDocstring(child.childForFieldName('body')),
379
+ exported: isPublic,
380
+ });
381
+ if (isPublic)
382
+ publicNames.push(name);
383
+ }
384
+ break;
385
+ }
386
+ case 'class_definition': {
387
+ const name = child.childForFieldName('name')?.text;
388
+ if (name) {
389
+ const isPublic = !name.startsWith('_');
390
+ out.topLevelDeclarations.push(`class ${name}`);
391
+ out.classes.push({
392
+ name,
393
+ kind: 'class',
394
+ methodCount: countPyMethods(child),
395
+ doc: pyDocstring(child.childForFieldName('body')),
396
+ exported: isPublic,
397
+ });
398
+ if (isPublic)
399
+ publicNames.push(name);
400
+ }
401
+ break;
402
+ }
403
+ case 'expression_statement': {
404
+ const assignment = child.namedChildren.find((c) => c?.type === 'assignment');
405
+ const left = assignment?.childForFieldName('left');
406
+ if (!assignment || !left || left.type !== 'identifier')
407
+ break;
408
+ const name = left.text;
409
+ if (name === '__all__') {
410
+ const right = assignment.childForFieldName('right');
411
+ if (right && (right.type === 'list' || right.type === 'tuple')) {
412
+ allList = [];
413
+ for (const item of right.namedChildren) {
414
+ if (item?.type !== 'string')
415
+ continue;
416
+ const content = item.namedChildren.find((c) => c?.type === 'string_content');
417
+ if (content)
418
+ allList.push(content.text);
419
+ }
420
+ }
421
+ }
422
+ else if (!name.startsWith('_')) {
423
+ publicNames.push(name);
424
+ if (/^[A-Z0-9_]+$/.test(name))
425
+ out.constNames.push(name);
426
+ }
427
+ break;
428
+ }
429
+ default:
430
+ break;
431
+ }
432
+ }
433
+ // `__all__` is the explicit export list; otherwise every public top-level
434
+ // binding (def/class/assignment without a leading underscore) is exported.
435
+ out.exports = allList ?? publicNames;
436
+ return dedupeResult(out);
437
+ }
438
+ // ---------------------------------------------------------------------------
439
+ // Go extraction
440
+ // ---------------------------------------------------------------------------
441
+ function isGoExported(name) {
442
+ return /^[A-Z]/.test(name);
443
+ }
444
+ /**
445
+ * First line of the contiguous `//` comment block directly above `node`
446
+ * (Go doc-comment convention: no blank line between comment and declaration).
447
+ */
448
+ function goDocComment(node) {
449
+ let expectedRow = node.startPosition.row;
450
+ let current = node.previousNamedSibling;
451
+ let top = null;
452
+ while (current && current.type === 'comment' && current.endPosition.row === expectedRow - 1) {
453
+ top = current;
454
+ expectedRow = current.startPosition.row;
455
+ current = current.previousNamedSibling;
456
+ }
457
+ return top ? commentFirstLine(top.text) : null;
458
+ }
459
+ function goImportPath(spec, out) {
460
+ const path = spec.childForFieldName('path');
461
+ if (path)
462
+ out.imports.push(stripQuotes(path.text));
463
+ }
464
+ /** Names declared in a `var_spec` / `const_spec` (may bind several identifiers). */
465
+ function goSpecNames(spec) {
466
+ const names = [];
467
+ for (const child of spec.namedChildren) {
468
+ if (child?.type === 'identifier')
469
+ names.push(child.text);
470
+ else
471
+ break; // identifiers come first; stop at the type/value part
472
+ }
473
+ return names;
474
+ }
475
+ /** Promote named types with methods to class-like entries, the rest to typeNames. */
476
+ function resolveNamedTypes(types, methodCounts, out) {
477
+ for (const entry of types) {
478
+ const methodCount = methodCounts.get(entry.name) ?? 0;
479
+ if (methodCount > 0) {
480
+ out.classes.push({ ...entry, methodCount });
481
+ }
482
+ else {
483
+ out.typeNames.push(entry.name);
484
+ }
485
+ }
486
+ }
487
+ function extractGo(root) {
488
+ const out = emptyResult();
489
+ const types = [];
490
+ const methodCounts = new Map();
491
+ for (const child of root.namedChildren) {
492
+ if (!child)
493
+ continue;
494
+ switch (child.type) {
495
+ case 'package_clause':
496
+ out.fileDoc = goDocComment(child);
497
+ break;
498
+ case 'import_declaration': {
499
+ for (const inner of child.namedChildren) {
500
+ if (!inner)
501
+ continue;
502
+ if (inner.type === 'import_spec')
503
+ goImportPath(inner, out);
504
+ else if (inner.type === 'import_spec_list') {
505
+ for (const spec of inner.namedChildren) {
506
+ if (spec?.type === 'import_spec')
507
+ goImportPath(spec, out);
508
+ }
509
+ }
510
+ }
511
+ break;
512
+ }
513
+ case 'function_declaration': {
514
+ const name = child.childForFieldName('name')?.text;
515
+ if (name) {
516
+ out.topLevelDeclarations.push(`func ${name}`);
517
+ out.functions.push({ name, doc: goDocComment(child), exported: isGoExported(name) });
518
+ if (isGoExported(name))
519
+ out.exports.push(name);
520
+ }
521
+ break;
522
+ }
523
+ case 'method_declaration': {
524
+ const name = child.childForFieldName('name')?.text;
525
+ if (name) {
526
+ out.topLevelDeclarations.push(`func ${name}`);
527
+ if (isGoExported(name))
528
+ out.exports.push(name);
529
+ // Attribute the method to its receiver's named type.
530
+ const receiver = child.childForFieldName('receiver');
531
+ const receiverType = receiver?.descendantsOfType('type_identifier')[0]?.text;
532
+ if (receiverType) {
533
+ methodCounts.set(receiverType, (methodCounts.get(receiverType) ?? 0) + 1);
534
+ }
535
+ }
536
+ break;
537
+ }
538
+ case 'type_declaration': {
539
+ for (const spec of child.namedChildren) {
540
+ if (!spec || (spec.type !== 'type_spec' && spec.type !== 'type_alias'))
541
+ continue;
542
+ const name = spec.childForFieldName('name')?.text;
543
+ if (!name)
544
+ continue;
545
+ const typeNode = spec.childForFieldName('type');
546
+ const kind = typeNode?.type === 'struct_type'
547
+ ? 'struct'
548
+ : typeNode?.type === 'interface_type'
549
+ ? 'interface'
550
+ : 'type';
551
+ out.topLevelDeclarations.push(kind === 'type' ? `type ${name}` : `type ${name} ${kind}`);
552
+ types.push({ name, kind, doc: goDocComment(child), exported: isGoExported(name) });
553
+ if (isGoExported(name))
554
+ out.exports.push(name);
555
+ }
556
+ break;
557
+ }
558
+ case 'var_declaration':
559
+ case 'const_declaration': {
560
+ const keyword = child.type === 'var_declaration' ? 'var' : 'const';
561
+ for (const spec of child.namedChildren) {
562
+ if (!spec || (spec.type !== 'var_spec' && spec.type !== 'const_spec'))
563
+ continue;
564
+ for (const name of goSpecNames(spec)) {
565
+ out.topLevelDeclarations.push(`${keyword} ${name}`);
566
+ if (keyword === 'const')
567
+ out.constNames.push(name);
568
+ if (isGoExported(name))
569
+ out.exports.push(name);
570
+ }
571
+ }
572
+ break;
573
+ }
574
+ default:
575
+ break;
576
+ }
577
+ }
578
+ resolveNamedTypes(types, methodCounts, out);
579
+ return dedupeResult(out);
580
+ }
581
+ // ---------------------------------------------------------------------------
582
+ // Rust extraction
583
+ // ---------------------------------------------------------------------------
584
+ /** Plain `pub` only — `pub(crate)` / `pub(super)` are not part of the public API. */
585
+ function isRustPub(node) {
586
+ return node.namedChildren.some((c) => c?.type === 'visibility_modifier' && c.text === 'pub');
587
+ }
588
+ /**
589
+ * First line of the contiguous `///` doc-comment block directly above `node`,
590
+ * skipping any `#[...]` attributes between the docs and the item.
591
+ */
592
+ function rustDocComment(node) {
593
+ let expectedRow = node.startPosition.row;
594
+ let current = node.previousNamedSibling;
595
+ while (current && current.type === 'attribute_item' && current.endPosition.row === expectedRow - 1) {
596
+ expectedRow = current.startPosition.row;
597
+ current = current.previousNamedSibling;
598
+ }
599
+ let top = null;
600
+ while (current &&
601
+ current.type === 'line_comment' &&
602
+ current.text.startsWith('///') &&
603
+ current.endPosition.row === expectedRow - 1) {
604
+ top = current;
605
+ expectedRow = current.startPosition.row;
606
+ current = current.previousNamedSibling;
607
+ }
608
+ if (top) {
609
+ const line = top.text.replace(/^\/\/\/\s*/, '').trim();
610
+ return line.length > 0 ? firstSentence(line) : null;
611
+ }
612
+ if (current && current.type === 'block_comment' && current.text.startsWith('/**')) {
613
+ return commentFirstLine(current.text);
614
+ }
615
+ return null;
616
+ }
617
+ /** Module path of a `use` argument, with grouped/aliased/glob tails removed. */
618
+ function rustUseBasePath(argument) {
619
+ switch (argument.type) {
620
+ case 'scoped_use_list': {
621
+ const path = argument.childForFieldName('path');
622
+ return path ? path.text : argument.text;
623
+ }
624
+ case 'use_as_clause': {
625
+ const path = argument.childForFieldName('path');
626
+ return path ? path.text : argument.text;
627
+ }
628
+ case 'use_wildcard': {
629
+ const inner = argument.namedChildren.find((c) => c !== null);
630
+ return inner ? inner.text : argument.text;
631
+ }
632
+ default:
633
+ return argument.text;
634
+ }
635
+ }
636
+ /** Visible names introduced by a `pub use` argument (re-exports). */
637
+ function rustUseNames(argument, out) {
638
+ switch (argument.type) {
639
+ case 'identifier':
640
+ case 'crate':
641
+ case 'self':
642
+ case 'super':
643
+ out.push(argument.text);
644
+ break;
645
+ case 'scoped_identifier': {
646
+ const name = argument.childForFieldName('name');
647
+ if (name)
648
+ out.push(name.text);
649
+ break;
650
+ }
651
+ case 'use_as_clause': {
652
+ const alias = argument.childForFieldName('alias');
653
+ if (alias)
654
+ out.push(alias.text);
655
+ break;
656
+ }
657
+ case 'scoped_use_list': {
658
+ const list = argument.childForFieldName('list');
659
+ for (const item of list?.namedChildren ?? []) {
660
+ if (item)
661
+ rustUseNames(item, out);
662
+ }
663
+ break;
664
+ }
665
+ case 'use_list': {
666
+ for (const item of argument.namedChildren) {
667
+ if (item)
668
+ rustUseNames(item, out);
669
+ }
670
+ break;
671
+ }
672
+ default:
673
+ break;
674
+ }
675
+ }
676
+ const RUST_TYPE_ITEMS = {
677
+ struct_item: 'struct',
678
+ enum_item: 'enum',
679
+ trait_item: 'trait',
680
+ union_item: 'union',
681
+ };
682
+ function extractRust(root) {
683
+ const out = emptyResult();
684
+ const types = [];
685
+ const methodCounts = new Map();
686
+ let seenCode = false;
687
+ for (const child of root.namedChildren) {
688
+ if (!child)
689
+ continue;
690
+ const pub = isRustPub(child);
691
+ switch (child.type) {
692
+ case 'line_comment':
693
+ if (!seenCode && out.fileDoc === null && child.text.startsWith('//!')) {
694
+ const line = child.text.replace(/^\/\/!\s*/, '').trim();
695
+ if (line.length > 0)
696
+ out.fileDoc = firstSentence(line);
697
+ }
698
+ continue; // comments don't count as code
699
+ case 'block_comment':
700
+ continue;
701
+ case 'use_declaration': {
702
+ const argument = child.childForFieldName('argument');
703
+ if (argument) {
704
+ out.imports.push(rustUseBasePath(argument));
705
+ if (pub)
706
+ rustUseNames(argument, out.exports); // `pub use` re-exports
707
+ }
708
+ break;
709
+ }
710
+ case 'mod_item': {
711
+ const name = child.childForFieldName('name')?.text;
712
+ if (name) {
713
+ out.topLevelDeclarations.push(`mod ${name}`);
714
+ if (!child.childForFieldName('body'))
715
+ out.imports.push(name); // `mod x;`
716
+ if (pub)
717
+ out.exports.push(name);
718
+ }
719
+ break;
720
+ }
721
+ case 'function_item': {
722
+ const name = child.childForFieldName('name')?.text;
723
+ if (name) {
724
+ out.topLevelDeclarations.push(`fn ${name}`);
725
+ out.functions.push({ name, doc: rustDocComment(child), exported: pub });
726
+ if (pub)
727
+ out.exports.push(name);
728
+ }
729
+ break;
730
+ }
731
+ case 'struct_item':
732
+ case 'enum_item':
733
+ case 'trait_item':
734
+ case 'union_item': {
735
+ const name = child.childForFieldName('name')?.text;
736
+ if (name) {
737
+ const kind = RUST_TYPE_ITEMS[child.type];
738
+ out.topLevelDeclarations.push(`${kind} ${name}`);
739
+ types.push({ name, kind, doc: rustDocComment(child), exported: pub });
740
+ if (pub)
741
+ out.exports.push(name);
742
+ }
743
+ break;
744
+ }
745
+ case 'impl_item': {
746
+ // Strip generic params so `impl Config<T>` matches `struct Config`.
747
+ const typeName = child.childForFieldName('type')?.text.replace(/<.*$/s, '');
748
+ if (typeName) {
749
+ out.topLevelDeclarations.push(`impl ${typeName}`);
750
+ const body = child.childForFieldName('body');
751
+ let methods = 0;
752
+ for (const item of body?.namedChildren ?? []) {
753
+ if (item?.type === 'function_item')
754
+ methods++;
755
+ }
756
+ if (methods > 0) {
757
+ methodCounts.set(typeName, (methodCounts.get(typeName) ?? 0) + methods);
758
+ }
759
+ }
760
+ break;
761
+ }
762
+ case 'type_item': {
763
+ const name = child.childForFieldName('name')?.text;
764
+ if (name) {
765
+ out.topLevelDeclarations.push(`type ${name}`);
766
+ out.typeNames.push(name);
767
+ if (pub)
768
+ out.exports.push(name);
769
+ }
770
+ break;
771
+ }
772
+ case 'const_item':
773
+ case 'static_item': {
774
+ const name = child.childForFieldName('name')?.text;
775
+ if (name) {
776
+ out.topLevelDeclarations.push(`${child.type === 'const_item' ? 'const' : 'static'} ${name}`);
777
+ out.constNames.push(name);
778
+ if (pub)
779
+ out.exports.push(name);
780
+ }
781
+ break;
782
+ }
783
+ default:
784
+ break;
785
+ }
786
+ seenCode = true;
787
+ }
788
+ resolveNamedTypes(types, methodCounts, out);
789
+ return dedupeResult(out);
790
+ }
791
+ function extract(root, grammar) {
792
+ switch (grammar) {
793
+ case 'python':
794
+ return extractPython(root);
795
+ case 'go':
796
+ return extractGo(root);
797
+ case 'rust':
798
+ return extractRust(root);
799
+ default:
800
+ return extractTsJs(root);
801
+ }
802
+ }
803
+ // ---------------------------------------------------------------------------
804
+ // Purpose line generation
805
+ // ---------------------------------------------------------------------------
806
+ function fileCategory(filePath, contents) {
807
+ const basename = getBasename(filePath);
808
+ const ext = getExtension(filePath);
809
+ const dir = filePath.replace(/\\/g, '/');
810
+ const inTestsDir = dir.includes('/tests/') || dir.includes('/test/') || /^tests?\//.test(dir);
811
+ if (ext === '.py') {
812
+ if (basename.startsWith('test_') || basename.endsWith('_test.py'))
813
+ return 'test';
814
+ if (basename === 'conftest.py')
815
+ return 'test';
816
+ if (inTestsDir)
817
+ return 'test';
818
+ if (basename === 'setup.py' || basename === 'settings.py' || basename === 'config.py') {
819
+ return 'config';
820
+ }
821
+ if (basename === '__init__.py' || basename === '__main__.py')
822
+ return 'entry point';
823
+ if (/\bif\s+__name__\s*==\s*['"]__main__['"]\s*:/.test(contents))
824
+ return 'entry point';
825
+ return null;
826
+ }
827
+ if (ext === '.go') {
828
+ if (basename.endsWith('_test.go'))
829
+ return 'test';
830
+ if (basename === 'main.go')
831
+ return 'entry point';
832
+ return null;
833
+ }
834
+ if (ext === '.rs') {
835
+ if (inTestsDir || basename.startsWith('test_'))
836
+ return 'test';
837
+ if (basename === 'main.rs' || basename === 'lib.rs' || basename === 'mod.rs') {
838
+ return 'entry point';
839
+ }
840
+ if (basename === 'build.rs')
841
+ return 'config';
842
+ return null;
843
+ }
844
+ if (filePath.endsWith('.d.ts'))
845
+ return 'types';
846
+ if (/\.(?:test|spec)\.[tj]sx?$/.test(basename))
847
+ return 'test';
848
+ if (/\.config\.[tj]sx?$/.test(basename) || /\.config\.mjs$/.test(basename))
849
+ return 'config';
850
+ if (basename === 'types.ts' || basename === 'interfaces.ts')
851
+ return 'types';
852
+ if (/^index\.[tj]sx?$/.test(basename))
853
+ return 'entry point';
854
+ return null;
855
+ }
856
+ function listNames(names, max) {
857
+ if (names.length <= max)
858
+ return names.join(', ');
859
+ return `${names.slice(0, max).join(', ')} (+${names.length - max} more)`;
860
+ }
861
+ function buildPurpose(filePath, contents, info) {
862
+ let category = fileCategory(filePath, contents);
863
+ // Executable entry points: shebang line or a top-level main() function.
864
+ if (category === null &&
865
+ (contents.startsWith('#!') || info.functions.some((f) => f.name === 'main'))) {
866
+ category = 'entry point';
867
+ }
868
+ let detail;
869
+ const exportedClasses = info.classes.filter((c) => c.exported);
870
+ const classes = exportedClasses.length > 0 ? exportedClasses : info.classes;
871
+ const exportedFns = info.functions.filter((f) => f.exported);
872
+ const fns = exportedFns.length > 0 ? exportedFns : info.functions;
873
+ if (classes.length > 0) {
874
+ const primary = [...classes].sort((a, b) => b.methodCount - a.methodCount)[0];
875
+ const desc = primary.doc ?? info.fileDoc;
876
+ const head = `${primary.kind} ${primary.name} (${primary.methodCount} method${primary.methodCount === 1 ? '' : 's'})`;
877
+ const rest = classes.filter((c) => c !== primary).map((c) => c.name);
878
+ const suffix = rest.length > 0 ? ` +${listNames(rest, 2)}` : '';
879
+ detail = desc ? `${head}${suffix}: ${desc}` : `${head}${suffix}`;
880
+ }
881
+ else if (fns.length > 0) {
882
+ const desc = fns[0].doc ?? info.fileDoc;
883
+ const head = fns.length === 1
884
+ ? `function ${fns[0].name}`
885
+ : `functions: ${listNames(fns.map((f) => f.name), 3)}`;
886
+ detail = desc ? `${head} — ${desc}` : head;
887
+ }
888
+ else if (info.typeNames.length > 0) {
889
+ const head = `types: ${listNames(info.typeNames, 4)}`;
890
+ detail = info.fileDoc ? `${head} — ${info.fileDoc}` : head;
891
+ }
892
+ else if (info.constNames.length > 0) {
893
+ const head = `constants: ${listNames(info.constNames, 4)}`;
894
+ detail = info.fileDoc ? `${head} — ${info.fileDoc}` : head;
895
+ }
896
+ else if (info.fileDoc) {
897
+ detail = info.fileDoc;
898
+ }
899
+ else {
900
+ detail = 'source';
901
+ }
902
+ // Avoid "types: types: ..." when the detail already leads with the category.
903
+ let purpose = category && !detail.startsWith(category) ? `${category}: ${detail}` : detail;
904
+ if (purpose.length > MAX_PURPOSE_LENGTH) {
905
+ purpose = `${purpose.slice(0, MAX_PURPOSE_LENGTH - 1)}…`;
906
+ }
907
+ return purpose;
908
+ }
909
+ // ---------------------------------------------------------------------------
910
+ // Entry point
911
+ // ---------------------------------------------------------------------------
912
+ /**
913
+ * Summarize a TS/JS, Python, Go or Rust file from its syntax tree. Falls back
914
+ * to the regex summarizer for unsupported extensions, empty files, parse
915
+ * errors, or when the WASM runtime cannot be loaded.
916
+ */
917
+ export async function summarizeFileAst(filePath, contents) {
918
+ const grammar = EXT_TO_GRAMMAR[getExtension(filePath)];
919
+ if (!grammar || runtimeBroken || contents.trim() === '') {
920
+ return summarizeFile(filePath, contents);
921
+ }
922
+ let parser;
923
+ try {
924
+ parser = await getParser(grammar);
925
+ }
926
+ catch {
927
+ // WASM runtime or grammar failed to load; don't retry per file.
928
+ runtimeBroken = true;
929
+ return summarizeFile(filePath, contents);
930
+ }
931
+ // Some grammars (Go) need a statement terminator after the last declaration;
932
+ // parse with a trailing newline so files missing one don't report errors.
933
+ const tree = parser.parse(contents.endsWith('\n') ? contents : `${contents}\n`);
934
+ if (!tree)
935
+ return summarizeFile(filePath, contents);
936
+ try {
937
+ if (tree.rootNode.hasError) {
938
+ return summarizeFile(filePath, contents);
939
+ }
940
+ const info = extract(tree.rootNode, grammar);
941
+ return {
942
+ purpose: buildPurpose(filePath, contents, info),
943
+ exports: info.exports,
944
+ imports: info.imports,
945
+ lineCount: contents.split('\n').length,
946
+ topLevelDeclarations: info.topLevelDeclarations,
947
+ confidence: 'high',
948
+ };
949
+ }
950
+ finally {
951
+ tree.delete();
952
+ }
953
+ }
954
+ /** True when the AST summarizer handles this file natively (vs regex fallback). */
955
+ export function isAstSupported(filePath) {
956
+ return getExtension(filePath) in EXT_TO_GRAMMAR;
957
+ }
958
+ //# sourceMappingURL=ast-summarizer.js.map