@kentwynn/kgraph 0.1.26 → 0.2.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 (41) hide show
  1. package/README.md +3 -18
  2. package/dist/cli/commands/context.d.ts +2 -2
  3. package/dist/cli/commands/context.js +82 -23
  4. package/dist/cli/commands/init.js +2 -25
  5. package/dist/cli/commands/workflow.js +2 -2
  6. package/dist/cli/help.d.ts +1 -0
  7. package/dist/cli/help.js +4 -6
  8. package/dist/cli/index.js +0 -2
  9. package/dist/cli/init-prompt.d.ts +2 -7
  10. package/dist/cli/init-prompt.js +0 -63
  11. package/dist/cli/init-recommendations.d.ts +1 -12
  12. package/dist/cli/init-recommendations.js +0 -23
  13. package/dist/cli/init-summary.d.ts +2 -4
  14. package/dist/cli/init-summary.js +10 -35
  15. package/dist/config/config.js +0 -33
  16. package/dist/context/context-query.js +23 -0
  17. package/dist/scanner/c-symbol-extractor.d.ts +1 -1
  18. package/dist/scanner/c-symbol-extractor.js +108 -65
  19. package/dist/scanner/csharp-symbol-extractor.d.ts +1 -1
  20. package/dist/scanner/csharp-symbol-extractor.js +93 -67
  21. package/dist/scanner/go-symbol-extractor.d.ts +1 -1
  22. package/dist/scanner/go-symbol-extractor.js +75 -60
  23. package/dist/scanner/jvm-symbol-extractor.d.ts +1 -1
  24. package/dist/scanner/jvm-symbol-extractor.js +139 -71
  25. package/dist/scanner/python-symbol-extractor.d.ts +1 -1
  26. package/dist/scanner/python-symbol-extractor.js +92 -71
  27. package/dist/scanner/repo-scanner.js +65 -8
  28. package/dist/scanner/rust-symbol-extractor.d.ts +1 -1
  29. package/dist/scanner/rust-symbol-extractor.js +94 -89
  30. package/dist/scanner/tree-sitter-parser.d.ts +5 -0
  31. package/dist/scanner/tree-sitter-parser.js +55 -0
  32. package/dist/types/cognition.d.ts +3 -2
  33. package/dist/types/config.d.ts +0 -7
  34. package/dist/types/maps.d.ts +6 -5
  35. package/package.json +10 -1
  36. package/dist/cli/commands/extractor.d.ts +0 -2
  37. package/dist/cli/commands/extractor.js +0 -50
  38. package/dist/extractors/extractor-registry.d.ts +0 -11
  39. package/dist/extractors/extractor-registry.js +0 -70
  40. package/dist/extractors/extractor-store.d.ts +0 -10
  41. package/dist/extractors/extractor-store.js +0 -58
@@ -1,16 +1,17 @@
1
+ import { parseSource } from './tree-sitter-parser.js';
1
2
  // Handles Java (.java) and Kotlin (.kt, .kts)
2
- export function extractJvmSymbols(sourceText, filePath) {
3
- const ext = filePath.endsWith('.kt') || filePath.endsWith('.kts') ? 'kotlin' : 'java';
4
- const lines = sourceText.split('\n');
3
+ export async function extractJvmSymbols(sourceText, filePath) {
5
4
  const symbols = [];
6
5
  const dependencies = [];
7
6
  const relationships = [];
8
7
  const warnings = [];
9
- // Stack tracks class/object/interface scope
10
- const typeStack = [];
11
- let braceDepth = 0;
12
- const addSymbol = (name, kind, lineNum, exported, parentName) => {
13
- const id = [filePath, kind, parentName, name, lineNum]
8
+ if (!sourceText.trim()) {
9
+ return { symbols, dependencies, relationships, warnings };
10
+ }
11
+ const lang = filePath.endsWith('.kt') || filePath.endsWith('.kts') ? 'kotlin' : 'java';
12
+ const tree = await parseSource(sourceText, lang);
13
+ const addSymbol = (name, kind, startLine, endLine, exported, parentName) => {
14
+ const id = [filePath, kind, parentName, name, startLine]
14
15
  .filter(Boolean)
15
16
  .join('#');
16
17
  symbols.push({
@@ -18,8 +19,8 @@ export function extractJvmSymbols(sourceText, filePath) {
18
19
  name,
19
20
  kind,
20
21
  filePath,
21
- startLine: lineNum,
22
- endLine: lineNum,
22
+ startLine,
23
+ endLine,
23
24
  exported,
24
25
  parentName,
25
26
  });
@@ -32,74 +33,141 @@ export function extractJvmSymbols(sourceText, filePath) {
32
33
  confidence: 'high',
33
34
  });
34
35
  };
35
- for (let i = 0; i < lines.length; i++) {
36
- const line = lines[i];
37
- const lineNum = i + 1;
38
- const trimmed = line.trim();
39
- if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
40
- continue;
41
- braceDepth +=
42
- (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
43
- while (typeStack.length > 0 &&
44
- braceDepth < typeStack[typeStack.length - 1].braceDepth) {
45
- typeStack.pop();
36
+ function hasModifier(node, modifier) {
37
+ const mods = node.childForFieldName('modifiers') ??
38
+ node.namedChildren.find((c) => c.type === 'modifiers');
39
+ if (!mods)
40
+ return false;
41
+ return mods.text.includes(modifier);
42
+ }
43
+ function walkJava(node, parentClassName) {
44
+ switch (node.type) {
45
+ case 'import_declaration': {
46
+ // Java import: scoped_identifier child
47
+ const scopedId = node.namedChildren.find((c) => c.type === 'scoped_identifier' || c.type === 'identifier');
48
+ if (scopedId) {
49
+ const specifier = scopedId.text.replace(/\.\*$/, '');
50
+ dependencies.push({ fromFile: filePath, specifier, kind: 'package' });
51
+ relationships.push({
52
+ sourceType: 'file',
53
+ sourceId: filePath,
54
+ targetType: 'package',
55
+ targetId: specifier,
56
+ relationshipType: 'import',
57
+ confidence: 'high',
58
+ });
59
+ }
60
+ return;
61
+ }
62
+ case 'class_declaration':
63
+ case 'interface_declaration':
64
+ case 'enum_declaration':
65
+ case 'annotation_type_declaration': {
66
+ const nameNode = node.childForFieldName('name');
67
+ if (nameNode) {
68
+ const exported = hasModifier(node, 'public');
69
+ addSymbol(nameNode.text, 'class', node.startPosition.row + 1, node.endPosition.row + 1, exported, parentClassName);
70
+ // Walk class body for methods and nested types
71
+ const body = node.childForFieldName('body') ??
72
+ node.namedChildren.find((c) => c.type === 'class_body' ||
73
+ c.type === 'interface_body' ||
74
+ c.type === 'enum_body');
75
+ if (body) {
76
+ for (const child of body.namedChildren) {
77
+ walkJava(child, nameNode.text);
78
+ }
79
+ }
80
+ }
81
+ return;
82
+ }
83
+ case 'method_declaration':
84
+ case 'constructor_declaration': {
85
+ const nameNode = node.childForFieldName('name');
86
+ if (nameNode) {
87
+ const exported = hasModifier(node, 'public');
88
+ const kind = parentClassName
89
+ ? 'method'
90
+ : 'function';
91
+ addSymbol(nameNode.text, kind, node.startPosition.row + 1, node.endPosition.row + 1, exported, parentClassName);
92
+ }
93
+ return;
94
+ }
46
95
  }
47
- // import statement
48
- const importMatch = trimmed.match(/^import\s+([\w.]+(?:\.\*)?)/);
49
- if (importMatch) {
50
- const specifier = importMatch[1].replace(/\.\*$/, '');
51
- dependencies.push({ fromFile: filePath, specifier, kind: 'package' });
52
- relationships.push({
53
- sourceType: 'file',
54
- sourceId: filePath,
55
- targetType: 'package',
56
- targetId: specifier,
57
- relationshipType: 'import',
58
- confidence: 'high',
59
- });
60
- continue;
96
+ for (const child of node.namedChildren) {
97
+ walkJava(child, parentClassName);
61
98
  }
62
- if (ext === 'java') {
63
- // class / interface / enum / @interface
64
- const typeMatch = trimmed.match(/\b(?:public\s+|private\s+|protected\s+|abstract\s+|final\s+)*(?:class|interface|enum|@interface)\s+(\w+)/);
65
- if (typeMatch) {
66
- const parent = typeStack[typeStack.length - 1];
67
- const exported = trimmed.includes('public');
68
- addSymbol(typeMatch[1], 'class', lineNum, exported, parent?.name);
69
- typeStack.push({ name: typeMatch[1], braceDepth });
70
- continue;
99
+ }
100
+ function walkKotlin(node, parentClassName) {
101
+ switch (node.type) {
102
+ case 'import': {
103
+ // Kotlin import: qualified_identifier child
104
+ const qualId = node.namedChildren.find((c) => c.type === 'qualified_identifier' || c.type === 'identifier');
105
+ if (qualId) {
106
+ const specifier = qualId.text;
107
+ dependencies.push({ fromFile: filePath, specifier, kind: 'package' });
108
+ relationships.push({
109
+ sourceType: 'file',
110
+ sourceId: filePath,
111
+ targetType: 'package',
112
+ targetId: specifier,
113
+ relationshipType: 'import',
114
+ confidence: 'high',
115
+ });
116
+ }
117
+ return;
71
118
  }
72
- // method: visibility returnType methodName(
73
- // Avoid matching field declarations (no parenthesis)
74
- const methodMatch = trimmed.match(/\b(?:public|private|protected|static|final|synchronized|abstract|native|default|void|@Override\s+(?:public|protected))\b.*\s+(\w+)\s*\(/);
75
- if (methodMatch && !trimmed.startsWith('//')) {
76
- const parent = typeStack[typeStack.length - 1];
77
- const exported = trimmed.includes('public');
78
- const kind = parent ? 'method' : 'function';
79
- addSymbol(methodMatch[1], kind, lineNum, exported, parent?.name);
80
- continue;
119
+ case 'class_declaration': {
120
+ const nameNode = node.childForFieldName('name') ??
121
+ node.namedChildren.find((c) => c.type === 'identifier');
122
+ if (nameNode) {
123
+ const exported = !hasModifier(node, 'private') && !hasModifier(node, 'internal');
124
+ addSymbol(nameNode.text, 'class', node.startPosition.row + 1, node.endPosition.row + 1, exported, parentClassName);
125
+ // Walk class body
126
+ const body = node.namedChildren.find((c) => c.type === 'class_body');
127
+ if (body) {
128
+ for (const child of body.namedChildren) {
129
+ walkKotlin(child, nameNode.text);
130
+ }
131
+ }
132
+ }
133
+ return;
81
134
  }
82
- }
83
- if (ext === 'kotlin') {
84
- // class / interface / object / data class / sealed class
85
- const typeMatch = trimmed.match(/\b(?:data\s+|sealed\s+|abstract\s+|open\s+|inner\s+)?(?:class|interface|object|enum\s+class)\s+(\w+)/);
86
- if (typeMatch) {
87
- const parent = typeStack[typeStack.length - 1];
88
- const exported = !trimmed.startsWith('private') && !trimmed.startsWith('internal');
89
- addSymbol(typeMatch[1], 'class', lineNum, exported, parent?.name);
90
- typeStack.push({ name: typeMatch[1], braceDepth });
91
- continue;
135
+ case 'object_declaration': {
136
+ const nameNode = node.namedChildren.find((c) => c.type === 'identifier');
137
+ if (nameNode) {
138
+ const exported = !hasModifier(node, 'private') && !hasModifier(node, 'internal');
139
+ addSymbol(nameNode.text, 'class', node.startPosition.row + 1, node.endPosition.row + 1, exported, parentClassName);
140
+ const body = node.namedChildren.find((c) => c.type === 'class_body');
141
+ if (body) {
142
+ for (const child of body.namedChildren) {
143
+ walkKotlin(child, nameNode.text);
144
+ }
145
+ }
146
+ }
147
+ return;
92
148
  }
93
- // fun
94
- const funcMatch = trimmed.match(/\bfun\s+(\w+)\s*[(<]/);
95
- if (funcMatch) {
96
- const parent = typeStack[typeStack.length - 1];
97
- const exported = !trimmed.startsWith('private') && !trimmed.startsWith('internal');
98
- const kind = parent ? 'method' : 'function';
99
- addSymbol(funcMatch[1], kind, lineNum, exported, parent?.name);
100
- continue;
149
+ case 'function_declaration': {
150
+ const nameNode = node.namedChildren.find((c) => c.type === 'identifier');
151
+ if (nameNode) {
152
+ const exported = !hasModifier(node, 'private') && !hasModifier(node, 'internal');
153
+ const kind = parentClassName
154
+ ? 'method'
155
+ : 'function';
156
+ addSymbol(nameNode.text, kind, node.startPosition.row + 1, node.endPosition.row + 1, exported, parentClassName);
157
+ }
158
+ return;
101
159
  }
102
160
  }
161
+ for (const child of node.namedChildren) {
162
+ walkKotlin(child, parentClassName);
163
+ }
164
+ }
165
+ if (lang === 'java') {
166
+ walkJava(tree.rootNode);
167
+ }
168
+ else {
169
+ walkKotlin(tree.rootNode);
103
170
  }
171
+ tree.delete();
104
172
  return { symbols, dependencies, relationships, warnings };
105
173
  }
@@ -1,2 +1,2 @@
1
1
  import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
- export declare function extractPythonSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
2
+ export declare function extractPythonSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;
@@ -1,13 +1,15 @@
1
- export function extractPythonSymbols(sourceText, filePath) {
2
- const lines = sourceText.split('\n');
1
+ import { parseSource } from './tree-sitter-parser.js';
2
+ export async function extractPythonSymbols(sourceText, filePath) {
3
3
  const symbols = [];
4
4
  const dependencies = [];
5
5
  const relationships = [];
6
6
  const warnings = [];
7
- // Stack tracks nested class scope: [{name, indent}]
8
- const classStack = [];
9
- const addSymbol = (name, kind, lineNum, parentName) => {
10
- const id = [filePath, kind, parentName, name, lineNum]
7
+ if (!sourceText.trim()) {
8
+ return { symbols, dependencies, relationships, warnings };
9
+ }
10
+ const tree = await parseSource(sourceText, 'python');
11
+ const addSymbol = (name, kind, startLine, endLine, exported, parentName) => {
12
+ const id = [filePath, kind, parentName, name, startLine]
11
13
  .filter(Boolean)
12
14
  .join('#');
13
15
  symbols.push({
@@ -15,9 +17,9 @@ export function extractPythonSymbols(sourceText, filePath) {
15
17
  name,
16
18
  kind,
17
19
  filePath,
18
- startLine: lineNum,
19
- endLine: lineNum,
20
- exported: false,
20
+ startLine,
21
+ endLine,
22
+ exported,
21
23
  parentName,
22
24
  });
23
25
  relationships.push({
@@ -29,70 +31,89 @@ export function extractPythonSymbols(sourceText, filePath) {
29
31
  confidence: 'high',
30
32
  });
31
33
  };
32
- for (let i = 0; i < lines.length; i++) {
33
- const line = lines[i];
34
- const lineNum = i + 1;
35
- const trimmed = line.trimStart();
36
- if (!trimmed || trimmed.startsWith('#'))
37
- continue;
38
- const indent = line.length - trimmed.length;
39
- // Pop classes we've exited: any class whose indent >= current line's indent
40
- // (unless this line IS the class that opened at that indent — handled by re-pushing below)
41
- while (classStack.length > 0 &&
42
- classStack[classStack.length - 1].indent >= indent) {
43
- classStack.pop();
44
- }
45
- // class definition
46
- const classMatch = trimmed.match(/^class\s+([A-Za-z_]\w*)/);
47
- if (classMatch) {
48
- const name = classMatch[1];
49
- const parent = classStack[classStack.length - 1];
50
- addSymbol(name, 'class', lineNum, parent?.name);
51
- classStack.push({ name, indent });
52
- continue;
53
- }
54
- // function / method definition (sync or async)
55
- const funcMatch = trimmed.match(/^(?:async\s+)?def\s+([A-Za-z_]\w*)/);
56
- if (funcMatch) {
57
- const name = funcMatch[1];
58
- const parent = classStack[classStack.length - 1];
59
- const kind = parent !== undefined ? 'method' : 'function';
60
- addSymbol(name, kind, lineNum, parent?.name);
61
- continue;
62
- }
63
- // import module
64
- const importMatch = trimmed.match(/^import\s+([\w.]+)/);
65
- if (importMatch) {
66
- const specifier = importMatch[1];
67
- dependencies.push({ fromFile: filePath, specifier, kind: 'package' });
68
- addSymbol(specifier, 'import', lineNum);
69
- relationships.push({
70
- sourceType: 'file',
71
- sourceId: filePath,
72
- targetType: 'package',
73
- targetId: specifier,
74
- relationshipType: 'import',
75
- confidence: 'high',
76
- });
77
- continue;
34
+ function walk(node, parentClassName) {
35
+ switch (node.type) {
36
+ case 'class_definition': {
37
+ const nameNode = node.childForFieldName('name');
38
+ if (nameNode) {
39
+ const startLine = node.startPosition.row + 1;
40
+ const endLine = node.endPosition.row + 1;
41
+ addSymbol(nameNode.text, 'class', startLine, endLine, false, parentClassName);
42
+ // Walk children for nested classes/methods
43
+ const body = node.childForFieldName('body');
44
+ if (body) {
45
+ for (const child of body.namedChildren) {
46
+ walk(child, nameNode.text);
47
+ }
48
+ }
49
+ }
50
+ return; // Don't recurse further from here
51
+ }
52
+ case 'function_definition': {
53
+ const nameNode = node.childForFieldName('name');
54
+ if (nameNode) {
55
+ const startLine = node.startPosition.row + 1;
56
+ const endLine = node.endPosition.row + 1;
57
+ const kind = parentClassName
58
+ ? 'method'
59
+ : 'function';
60
+ addSymbol(nameNode.text, kind, startLine, endLine, false, parentClassName);
61
+ }
62
+ return; // Don't recurse into function bodies
63
+ }
64
+ case 'import_statement': {
65
+ // import os / import os.path
66
+ const startLine = node.startPosition.row + 1;
67
+ for (const child of node.namedChildren) {
68
+ if (child.type === 'dotted_name' || child.type === 'aliased_import') {
69
+ const specifier = child.type === 'aliased_import'
70
+ ? (child.childForFieldName('name')?.text ?? child.text)
71
+ : child.text;
72
+ dependencies.push({
73
+ fromFile: filePath,
74
+ specifier,
75
+ kind: 'package',
76
+ });
77
+ addSymbol(specifier, 'import', startLine, startLine, false);
78
+ relationships.push({
79
+ sourceType: 'file',
80
+ sourceId: filePath,
81
+ targetType: 'package',
82
+ targetId: specifier,
83
+ relationshipType: 'import',
84
+ confidence: 'high',
85
+ });
86
+ }
87
+ }
88
+ return;
89
+ }
90
+ case 'import_from_statement': {
91
+ // from X import Y
92
+ const moduleNode = node.childForFieldName('module_name');
93
+ if (moduleNode) {
94
+ const specifier = moduleNode.text;
95
+ const kind = specifier.startsWith('.')
96
+ ? 'local'
97
+ : 'package';
98
+ dependencies.push({ fromFile: filePath, specifier, kind });
99
+ relationships.push({
100
+ sourceType: 'file',
101
+ sourceId: filePath,
102
+ targetType: kind === 'local' ? 'file' : 'package',
103
+ targetId: specifier,
104
+ relationshipType: 'import',
105
+ confidence: kind === 'local' ? 'medium' : 'high',
106
+ });
107
+ }
108
+ return;
109
+ }
78
110
  }
79
- // from X import Y
80
- const fromMatch = trimmed.match(/^from\s+(\S+)\s+import/);
81
- if (fromMatch) {
82
- const specifier = fromMatch[1];
83
- const kind = specifier.startsWith('.')
84
- ? 'local'
85
- : 'package';
86
- dependencies.push({ fromFile: filePath, specifier, kind });
87
- relationships.push({
88
- sourceType: 'file',
89
- sourceId: filePath,
90
- targetType: kind === 'local' ? 'file' : 'package',
91
- targetId: specifier,
92
- relationshipType: 'import',
93
- confidence: kind === 'local' ? 'medium' : 'high',
94
- });
111
+ // Recurse into children for top-level statements
112
+ for (const child of node.namedChildren) {
113
+ walk(child, parentClassName);
95
114
  }
96
115
  }
116
+ walk(tree.rootNode);
117
+ tree.delete();
97
118
  return { symbols, dependencies, relationships, warnings };
98
119
  }
@@ -2,6 +2,7 @@ import fg from 'fast-glob';
2
2
  import crypto from 'node:crypto';
3
3
  import { readFile, stat } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { estimateTokens } from '../session/token-estimator.js';
5
6
  import { extractCSymbols } from './c-symbol-extractor.js';
6
7
  import { extractCSharpSymbols } from './csharp-symbol-extractor.js';
7
8
  import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
@@ -10,10 +11,9 @@ import { extractJvmSymbols } from './jvm-symbol-extractor.js';
10
11
  import { extractPythonSymbols } from './python-symbol-extractor.js';
11
12
  import { extractRustSymbols } from './rust-symbol-extractor.js';
12
13
  import { extractTsSymbols } from './ts-symbol-extractor.js';
13
- import { estimateTokens } from '../session/token-estimator.js';
14
14
  const C_EXTS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx']);
15
15
  const JVM_EXTS = new Set(['.java', '.kt', '.kts']);
16
- function extractSymbols(text, repoPath) {
16
+ async function extractSymbols(text, repoPath) {
17
17
  const ext = path.extname(repoPath);
18
18
  if (ext === '.py' || ext === '.pyw' || ext === '.pyi') {
19
19
  return extractPythonSymbols(text, repoPath);
@@ -46,21 +46,71 @@ export async function scanRepository(rootPath, config, previous) {
46
46
  unique: true,
47
47
  ignore: buildFastGlobIgnore(allExcludes),
48
48
  });
49
+ // Build lookup maps from previous scan for incremental skip
50
+ const prevFileByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
51
+ const prevSymbolsByFile = new Map();
52
+ const prevDepsByFile = new Map();
53
+ const prevRelsBySource = new Map();
54
+ if (previous) {
55
+ for (const sym of previous.symbols) {
56
+ const arr = prevSymbolsByFile.get(sym.filePath) ?? [];
57
+ arr.push(sym);
58
+ prevSymbolsByFile.set(sym.filePath, arr);
59
+ }
60
+ for (const dep of previous.dependencies) {
61
+ const arr = prevDepsByFile.get(dep.fromFile) ?? [];
62
+ arr.push(dep);
63
+ prevDepsByFile.set(dep.fromFile, arr);
64
+ }
65
+ for (const rel of previous.relationships) {
66
+ if (rel.relationshipType !== 'import' &&
67
+ rel.relationshipType !== 'moved-from') {
68
+ const arr = prevRelsBySource.get(rel.sourceId) ?? [];
69
+ arr.push(rel);
70
+ prevRelsBySource.set(rel.sourceId, arr);
71
+ }
72
+ }
73
+ }
49
74
  const files = [];
50
75
  const symbols = [];
51
76
  const dependencies = [];
52
77
  const relationships = [];
53
78
  const warnings = [];
79
+ let skippedFiles = 0;
54
80
  for (const repoPath of entries.sort()) {
55
81
  if (shouldExclude(repoPath, mergedConfig)) {
56
82
  continue;
57
83
  }
58
84
  const absolutePath = path.join(rootPath, repoPath);
59
85
  try {
60
- const [info, content] = await Promise.all([
61
- stat(absolutePath),
62
- readFile(absolutePath),
63
- ]);
86
+ const info = await stat(absolutePath);
87
+ // Incremental skip: if mtime and size match previous, carry forward
88
+ const prevFile = prevFileByPath.get(repoPath);
89
+ if (prevFile &&
90
+ prevFile.sizeBytes === info.size &&
91
+ prevFile.modifiedAt === info.mtime.toISOString()) {
92
+ files.push({ ...prevFile, modifiedAt: info.mtime.toISOString() });
93
+ const prevSyms = prevSymbolsByFile.get(repoPath);
94
+ if (prevSyms)
95
+ symbols.push(...prevSyms);
96
+ const prevDeps = prevDepsByFile.get(repoPath);
97
+ if (prevDeps)
98
+ dependencies.push(...prevDeps);
99
+ const prevRels = prevRelsBySource.get(repoPath);
100
+ if (prevRels)
101
+ relationships.push(...prevRels);
102
+ // Also carry forward symbol-sourced relationships
103
+ if (prevSyms) {
104
+ for (const sym of prevSyms) {
105
+ const symRels = prevRelsBySource.get(sym.id);
106
+ if (symRels)
107
+ relationships.push(...symRels);
108
+ }
109
+ }
110
+ skippedFiles++;
111
+ continue;
112
+ }
113
+ const content = await readFile(absolutePath);
64
114
  const text = content.toString('utf8');
65
115
  const contentHash = crypto
66
116
  .createHash('sha256')
@@ -79,7 +129,7 @@ export async function scanRepository(rootPath, config, previous) {
79
129
  warnings: [],
80
130
  };
81
131
  if (isPreciseLanguage(repoPath, config)) {
82
- const extracted = extractSymbols(text, repoPath);
132
+ const extracted = await extractSymbols(text, repoPath);
83
133
  symbols.push(...extracted.symbols);
84
134
  dependencies.push(...extracted.dependencies);
85
135
  relationships.push(...extracted.relationships.filter((relationship) => relationship.relationshipType !== 'import'));
@@ -105,7 +155,14 @@ export async function scanRepository(rootPath, config, previous) {
105
155
  resolveLocalDependencies(dependencies, files);
106
156
  relationships.push(...buildImportRelationships(dependencies));
107
157
  relationships.push(...detectMovedFiles(previous?.files ?? [], files));
108
- return { files, symbols, dependencies, relationships, warnings };
158
+ return {
159
+ files,
160
+ symbols,
161
+ dependencies,
162
+ relationships,
163
+ warnings,
164
+ skippedFiles,
165
+ };
109
166
  }
110
167
  const SOURCE_EXTENSIONS = [
111
168
  '.ts',
@@ -1,2 +1,2 @@
1
1
  import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
- export declare function extractRustSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
2
+ export declare function extractRustSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;