@kentwynn/kgraph 0.1.11 → 0.1.12
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.
- package/README.md +9 -7
- package/dist/config/config.js +29 -1
- package/dist/context/ranking.js +6 -3
- package/dist/scanner/c-symbol-extractor.d.ts +2 -0
- package/dist/scanner/c-symbol-extractor.js +96 -0
- package/dist/scanner/csharp-symbol-extractor.d.ts +2 -0
- package/dist/scanner/csharp-symbol-extractor.js +96 -0
- package/dist/scanner/go-symbol-extractor.d.ts +2 -0
- package/dist/scanner/go-symbol-extractor.js +98 -0
- package/dist/scanner/jvm-symbol-extractor.d.ts +2 -0
- package/dist/scanner/jvm-symbol-extractor.js +105 -0
- package/dist/scanner/python-symbol-extractor.d.ts +2 -0
- package/dist/scanner/python-symbol-extractor.js +98 -0
- package/dist/scanner/repo-scanner.js +31 -1
- package/dist/scanner/rust-symbol-extractor.d.ts +2 -0
- package/dist/scanner/rust-symbol-extractor.js +119 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,12 +149,12 @@ kgraph integrate add codex copilot cursor claude-code
|
|
|
149
149
|
kgraph integrate list
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
| Tool | Always-on instruction | Skills / commands
|
|
153
|
-
| -------------- | --------------------------------- |
|
|
154
|
-
| GitHub Copilot | `.github/copilot-instructions.md` | `/kgraph-scan` · `/kgraph-update` · `/kgraph-visualize`
|
|
155
|
-
| Codex | `AGENTS.md` | `.agents/skills/kgraph/SKILL.md` (VS Code Agent Skills standard)
|
|
156
|
-
| Cursor | `.cursor/rules/kgraph.mdc` | Built into the rule
|
|
157
|
-
| Claude Code | `CLAUDE.md` | `/kgraph` · `/kgraph-scan` · `/kgraph-update` · `/kgraph-visualize` |
|
|
152
|
+
| Tool | Always-on instruction | Skills / commands |
|
|
153
|
+
| -------------- | --------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
154
|
+
| GitHub Copilot | `.github/copilot-instructions.md` | `/kgraph-scan` · `/kgraph-update` · `/kgraph-visualize` · `/kgraph-history` · `/kgraph-capture` |
|
|
155
|
+
| Codex | `AGENTS.md` | `.agents/skills/kgraph/SKILL.md` (VS Code Agent Skills standard) |
|
|
156
|
+
| Cursor | `.cursor/rules/kgraph.mdc` | Built into the rule |
|
|
157
|
+
| Claude Code | `CLAUDE.md` | `/kgraph` · `/kgraph-scan` · `/kgraph-update` · `/kgraph-visualize` · `/kgraph-history` |
|
|
158
158
|
|
|
159
159
|
Each integration installs a `/kgraph` skill or command that handles the full workflow automatically: load context → work → capture findings → update cognition. `/kgraph-scan`, `/kgraph-update`, and `/kgraph-visualize` are available for manual maintenance.
|
|
160
160
|
|
|
@@ -198,7 +198,9 @@ kgraph history --json # machine-readable output
|
|
|
198
198
|
| **Relationships** | call sites, re-exports, shared types |
|
|
199
199
|
| **Cognition** | past decisions, architectural constraints, debugging insights, gotchas |
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
**Deep scan** (symbols, functions, classes, methods, imports): TypeScript, JavaScript, Python, Go, Rust, Java, Kotlin, C, C++, C#
|
|
202
|
+
|
|
203
|
+
**Generic scan** (file path, language, size — contributes to context): Ruby, PHP, Swift, Shell, HTML, CSS, SQL, YAML, TOML, and 20+ more — detected by file extension, no configuration needed.
|
|
202
204
|
|
|
203
205
|
---
|
|
204
206
|
|
package/dist/config/config.js
CHANGED
|
@@ -35,7 +35,35 @@ export const DEFAULT_CONFIG = {
|
|
|
35
35
|
'.DS_Store',
|
|
36
36
|
],
|
|
37
37
|
languages: {
|
|
38
|
-
precise: [
|
|
38
|
+
precise: [
|
|
39
|
+
// JavaScript / TypeScript
|
|
40
|
+
'.js',
|
|
41
|
+
'.jsx',
|
|
42
|
+
'.ts',
|
|
43
|
+
'.tsx',
|
|
44
|
+
// Python
|
|
45
|
+
'.py',
|
|
46
|
+
'.pyw',
|
|
47
|
+
'.pyi',
|
|
48
|
+
// Go
|
|
49
|
+
'.go',
|
|
50
|
+
// Rust
|
|
51
|
+
'.rs',
|
|
52
|
+
// Java / Kotlin
|
|
53
|
+
'.java',
|
|
54
|
+
'.kt',
|
|
55
|
+
'.kts',
|
|
56
|
+
// C / C++
|
|
57
|
+
'.c',
|
|
58
|
+
'.h',
|
|
59
|
+
'.cpp',
|
|
60
|
+
'.cc',
|
|
61
|
+
'.cxx',
|
|
62
|
+
'.hpp',
|
|
63
|
+
'.hxx',
|
|
64
|
+
// C#
|
|
65
|
+
'.cs',
|
|
66
|
+
],
|
|
39
67
|
},
|
|
40
68
|
maxContextItems: 8,
|
|
41
69
|
domainHints: {},
|
package/dist/context/ranking.js
CHANGED
|
@@ -14,11 +14,14 @@ export function rankByFields(query, items, fields) {
|
|
|
14
14
|
for (const field of fields) {
|
|
15
15
|
const value = field.value(item);
|
|
16
16
|
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
17
|
-
const haystack = values.join(
|
|
17
|
+
const haystack = values.join(' ').toLowerCase();
|
|
18
18
|
for (const token of tokens) {
|
|
19
19
|
if (haystack.includes(token)) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const baseScore = field.name === 'path' || field.name === 'name' ? 3 : 1;
|
|
21
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
|
+
const wordBoundary = new RegExp(`\\b${escaped}\\b`).test(haystack);
|
|
23
|
+
score += baseScore + (wordBoundary ? 2 : 0);
|
|
24
|
+
reasons.push(`${field.name} matched "${token}"${wordBoundary ? ' (exact)' : ''}`);
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Handles C (.c, .h) and C++ (.cpp, .cc, .cxx, .hpp, .hxx)
|
|
2
|
+
export function extractCSymbols(sourceText, filePath) {
|
|
3
|
+
const lines = sourceText.split('\n');
|
|
4
|
+
const symbols = [];
|
|
5
|
+
const dependencies = [];
|
|
6
|
+
const relationships = [];
|
|
7
|
+
const warnings = [];
|
|
8
|
+
const typeStack = [];
|
|
9
|
+
let braceDepth = 0;
|
|
10
|
+
const addSymbol = (name, kind, lineNum, parentName) => {
|
|
11
|
+
const id = [filePath, kind, parentName, name, lineNum]
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.join('#');
|
|
14
|
+
symbols.push({
|
|
15
|
+
id,
|
|
16
|
+
name,
|
|
17
|
+
kind,
|
|
18
|
+
filePath,
|
|
19
|
+
startLine: lineNum,
|
|
20
|
+
endLine: lineNum,
|
|
21
|
+
exported: false,
|
|
22
|
+
parentName,
|
|
23
|
+
});
|
|
24
|
+
relationships.push({
|
|
25
|
+
sourceType: 'file',
|
|
26
|
+
sourceId: filePath,
|
|
27
|
+
targetType: 'symbol',
|
|
28
|
+
targetId: id,
|
|
29
|
+
relationshipType: 'contains',
|
|
30
|
+
confidence: 'high',
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
const line = lines[i];
|
|
35
|
+
const lineNum = i + 1;
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
38
|
+
continue;
|
|
39
|
+
braceDepth +=
|
|
40
|
+
(line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
|
|
41
|
+
while (typeStack.length > 0 &&
|
|
42
|
+
braceDepth < typeStack[typeStack.length - 1].braceDepth) {
|
|
43
|
+
typeStack.pop();
|
|
44
|
+
}
|
|
45
|
+
// #include <...> or #include "..."
|
|
46
|
+
const includeMatch = trimmed.match(/^#include\s+[<"]([^>"]+)[>"]/);
|
|
47
|
+
if (includeMatch) {
|
|
48
|
+
const specifier = includeMatch[1];
|
|
49
|
+
const kind = trimmed.includes('"') ? 'local' : 'package';
|
|
50
|
+
dependencies.push({ fromFile: filePath, specifier, kind });
|
|
51
|
+
relationships.push({
|
|
52
|
+
sourceType: 'file',
|
|
53
|
+
sourceId: filePath,
|
|
54
|
+
targetType: kind === 'local' ? 'file' : 'package',
|
|
55
|
+
targetId: specifier,
|
|
56
|
+
relationshipType: 'import',
|
|
57
|
+
confidence: 'high',
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// class / struct (C++ with body — has name before {)
|
|
62
|
+
const classMatch = trimmed.match(/\b(?:class|struct)\s+(\w+)\s*(?::[^{]*)?\s*\{/);
|
|
63
|
+
if (classMatch) {
|
|
64
|
+
addSymbol(classMatch[1], 'class', lineNum, typeStack[typeStack.length - 1]?.name);
|
|
65
|
+
typeStack.push({ name: classMatch[1], braceDepth });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Function definition: returnType funcName( — must have ( and no ; on same line
|
|
69
|
+
// Exclude preprocessor, declarations without body
|
|
70
|
+
if (!trimmed.endsWith(';') && !trimmed.startsWith('#')) {
|
|
71
|
+
const funcMatch = trimmed.match(/\b(\w+)\s*\((?:[^)]*)?\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:final\s*)?\{?$/);
|
|
72
|
+
// Filter out common false positives: if/for/while/switch/catch/else
|
|
73
|
+
const CONTROL_FLOW = new Set([
|
|
74
|
+
'if',
|
|
75
|
+
'for',
|
|
76
|
+
'while',
|
|
77
|
+
'switch',
|
|
78
|
+
'catch',
|
|
79
|
+
'else',
|
|
80
|
+
'return',
|
|
81
|
+
'sizeof',
|
|
82
|
+
'typeof',
|
|
83
|
+
]);
|
|
84
|
+
if (funcMatch &&
|
|
85
|
+
!CONTROL_FLOW.has(funcMatch[1]) &&
|
|
86
|
+
funcMatch[1] !== 'class' &&
|
|
87
|
+
funcMatch[1] !== 'struct') {
|
|
88
|
+
const parent = typeStack[typeStack.length - 1];
|
|
89
|
+
const kind = parent ? 'method' : 'function';
|
|
90
|
+
addSymbol(funcMatch[1], kind, lineNum, parent?.name);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { symbols, dependencies, relationships, warnings };
|
|
96
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function extractCSharpSymbols(sourceText, filePath) {
|
|
2
|
+
const lines = sourceText.split('\n');
|
|
3
|
+
const symbols = [];
|
|
4
|
+
const dependencies = [];
|
|
5
|
+
const relationships = [];
|
|
6
|
+
const warnings = [];
|
|
7
|
+
const typeStack = [];
|
|
8
|
+
let braceDepth = 0;
|
|
9
|
+
const addSymbol = (name, kind, lineNum, exported, parentName) => {
|
|
10
|
+
const id = [filePath, kind, parentName, name, lineNum]
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.join('#');
|
|
13
|
+
symbols.push({
|
|
14
|
+
id,
|
|
15
|
+
name,
|
|
16
|
+
kind,
|
|
17
|
+
filePath,
|
|
18
|
+
startLine: lineNum,
|
|
19
|
+
endLine: lineNum,
|
|
20
|
+
exported,
|
|
21
|
+
parentName,
|
|
22
|
+
});
|
|
23
|
+
relationships.push({
|
|
24
|
+
sourceType: 'file',
|
|
25
|
+
sourceId: filePath,
|
|
26
|
+
targetType: 'symbol',
|
|
27
|
+
targetId: id,
|
|
28
|
+
relationshipType: 'contains',
|
|
29
|
+
confidence: 'high',
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
const lineNum = i + 1;
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
37
|
+
continue;
|
|
38
|
+
braceDepth +=
|
|
39
|
+
(line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
|
|
40
|
+
while (typeStack.length > 0 &&
|
|
41
|
+
braceDepth < typeStack[typeStack.length - 1].braceDepth) {
|
|
42
|
+
typeStack.pop();
|
|
43
|
+
}
|
|
44
|
+
// using statement
|
|
45
|
+
const usingMatch = trimmed.match(/^using\s+([\w.]+)\s*;/);
|
|
46
|
+
if (usingMatch) {
|
|
47
|
+
const specifier = usingMatch[1];
|
|
48
|
+
dependencies.push({ fromFile: filePath, specifier, kind: 'package' });
|
|
49
|
+
relationships.push({
|
|
50
|
+
sourceType: 'file',
|
|
51
|
+
sourceId: filePath,
|
|
52
|
+
targetType: 'package',
|
|
53
|
+
targetId: specifier,
|
|
54
|
+
relationshipType: 'import',
|
|
55
|
+
confidence: 'high',
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// class / interface / struct / enum / record
|
|
60
|
+
const typeMatch = trimmed.match(/\b(?:public|private|protected|internal|static|abstract|sealed|partial|readonly)?\s*(?:public|private|protected|internal|static|abstract|sealed|partial|readonly)?\s*(?:class|interface|struct|enum|record)\s+(\w+)/);
|
|
61
|
+
if (typeMatch && typeMatch[1]) {
|
|
62
|
+
const parent = typeStack[typeStack.length - 1];
|
|
63
|
+
const exported = trimmed.includes('public') || trimmed.includes('internal');
|
|
64
|
+
addSymbol(typeMatch[1], 'class', lineNum, exported, parent?.name);
|
|
65
|
+
typeStack.push({ name: typeMatch[1], braceDepth });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// method: visibility [modifiers] returnType MethodName(
|
|
69
|
+
// Must have parens, no semicolon (not a field), not a control-flow keyword
|
|
70
|
+
const CONTROL_FLOW = new Set([
|
|
71
|
+
'if',
|
|
72
|
+
'for',
|
|
73
|
+
'foreach',
|
|
74
|
+
'while',
|
|
75
|
+
'switch',
|
|
76
|
+
'catch',
|
|
77
|
+
'else',
|
|
78
|
+
'using',
|
|
79
|
+
'lock',
|
|
80
|
+
'return',
|
|
81
|
+
]);
|
|
82
|
+
if (!trimmed.endsWith(';')) {
|
|
83
|
+
// Strip generic type parameters (e.g. Task<string> → Task) before matching method name
|
|
84
|
+
const normalizedForMethod = trimmed.replace(/<[^>]*>/g, '');
|
|
85
|
+
const methodMatch = normalizedForMethod.match(/\b(?:public|private|protected|internal|static|virtual|override|abstract|async|new|sealed)[\w\s]*\s+(\w+)\s*\(/);
|
|
86
|
+
if (methodMatch && !CONTROL_FLOW.has(methodMatch[1])) {
|
|
87
|
+
const parent = typeStack[typeStack.length - 1];
|
|
88
|
+
const exported = trimmed.includes('public') || trimmed.includes('internal');
|
|
89
|
+
const kind = parent ? 'method' : 'function';
|
|
90
|
+
addSymbol(methodMatch[1], kind, lineNum, exported, parent?.name);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { symbols, dependencies, relationships, warnings };
|
|
96
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function extractGoSymbols(sourceText, filePath) {
|
|
2
|
+
const lines = sourceText.split('\n');
|
|
3
|
+
const symbols = [];
|
|
4
|
+
const dependencies = [];
|
|
5
|
+
const relationships = [];
|
|
6
|
+
const warnings = [];
|
|
7
|
+
const addSymbol = (name, kind, lineNum, parentName) => {
|
|
8
|
+
const id = [filePath, kind, parentName, name, lineNum]
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.join('#');
|
|
11
|
+
symbols.push({
|
|
12
|
+
id,
|
|
13
|
+
name,
|
|
14
|
+
kind,
|
|
15
|
+
filePath,
|
|
16
|
+
startLine: lineNum,
|
|
17
|
+
endLine: lineNum,
|
|
18
|
+
exported: /^[A-Z]/.test(name), // Go: exported = starts with uppercase
|
|
19
|
+
parentName,
|
|
20
|
+
});
|
|
21
|
+
relationships.push({
|
|
22
|
+
sourceType: 'file',
|
|
23
|
+
sourceId: filePath,
|
|
24
|
+
targetType: 'symbol',
|
|
25
|
+
targetId: id,
|
|
26
|
+
relationshipType: 'contains',
|
|
27
|
+
confidence: 'high',
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
let inImportBlock = false;
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const line = lines[i];
|
|
33
|
+
const lineNum = i + 1;
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
36
|
+
continue;
|
|
37
|
+
// import block: import ( ... )
|
|
38
|
+
if (trimmed === 'import (') {
|
|
39
|
+
inImportBlock = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (inImportBlock) {
|
|
43
|
+
if (trimmed === ')') {
|
|
44
|
+
inImportBlock = false;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// e.g. "fmt" or aliased: log "log/slog"
|
|
48
|
+
const specMatch = trimmed.match(/"([^"]+)"/);
|
|
49
|
+
if (specMatch) {
|
|
50
|
+
const specifier = specMatch[1];
|
|
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
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// single import: import "fmt"
|
|
64
|
+
const singleImport = trimmed.match(/^import\s+"([^"]+)"/);
|
|
65
|
+
if (singleImport) {
|
|
66
|
+
const specifier = singleImport[1];
|
|
67
|
+
dependencies.push({ fromFile: filePath, specifier, kind: 'package' });
|
|
68
|
+
relationships.push({
|
|
69
|
+
sourceType: 'file',
|
|
70
|
+
sourceId: filePath,
|
|
71
|
+
targetType: 'package',
|
|
72
|
+
targetId: specifier,
|
|
73
|
+
relationshipType: 'import',
|
|
74
|
+
confidence: 'high',
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// method with receiver: func (r ReceiverType) MethodName(
|
|
79
|
+
const methodMatch = trimmed.match(/^func\s+\(\s*\w+\s+\*?(\w+)\s*\)\s+(\w+)\s*\(/);
|
|
80
|
+
if (methodMatch) {
|
|
81
|
+
addSymbol(methodMatch[2], 'method', lineNum, methodMatch[1]);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// top-level function: func FuncName(
|
|
85
|
+
const funcMatch = trimmed.match(/^func\s+(\w+)\s*\(/);
|
|
86
|
+
if (funcMatch) {
|
|
87
|
+
addSymbol(funcMatch[1], 'function', lineNum);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// type declaration: type Name struct / type Name interface / type Name ...
|
|
91
|
+
const typeMatch = trimmed.match(/^type\s+(\w+)\s+/);
|
|
92
|
+
if (typeMatch) {
|
|
93
|
+
addSymbol(typeMatch[1], 'class', lineNum); // 'class' is closest kind for struct/interface
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { symbols, dependencies, relationships, warnings };
|
|
98
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// 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');
|
|
5
|
+
const symbols = [];
|
|
6
|
+
const dependencies = [];
|
|
7
|
+
const relationships = [];
|
|
8
|
+
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]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join('#');
|
|
16
|
+
symbols.push({
|
|
17
|
+
id,
|
|
18
|
+
name,
|
|
19
|
+
kind,
|
|
20
|
+
filePath,
|
|
21
|
+
startLine: lineNum,
|
|
22
|
+
endLine: lineNum,
|
|
23
|
+
exported,
|
|
24
|
+
parentName,
|
|
25
|
+
});
|
|
26
|
+
relationships.push({
|
|
27
|
+
sourceType: 'file',
|
|
28
|
+
sourceId: filePath,
|
|
29
|
+
targetType: 'symbol',
|
|
30
|
+
targetId: id,
|
|
31
|
+
relationshipType: 'contains',
|
|
32
|
+
confidence: 'high',
|
|
33
|
+
});
|
|
34
|
+
};
|
|
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();
|
|
46
|
+
}
|
|
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;
|
|
61
|
+
}
|
|
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;
|
|
71
|
+
}
|
|
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;
|
|
81
|
+
}
|
|
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;
|
|
92
|
+
}
|
|
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;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { symbols, dependencies, relationships, warnings };
|
|
105
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function extractPythonSymbols(sourceText, filePath) {
|
|
2
|
+
const lines = sourceText.split('\n');
|
|
3
|
+
const symbols = [];
|
|
4
|
+
const dependencies = [];
|
|
5
|
+
const relationships = [];
|
|
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]
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.join('#');
|
|
13
|
+
symbols.push({
|
|
14
|
+
id,
|
|
15
|
+
name,
|
|
16
|
+
kind,
|
|
17
|
+
filePath,
|
|
18
|
+
startLine: lineNum,
|
|
19
|
+
endLine: lineNum,
|
|
20
|
+
exported: false,
|
|
21
|
+
parentName,
|
|
22
|
+
});
|
|
23
|
+
relationships.push({
|
|
24
|
+
sourceType: 'file',
|
|
25
|
+
sourceId: filePath,
|
|
26
|
+
targetType: 'symbol',
|
|
27
|
+
targetId: id,
|
|
28
|
+
relationshipType: 'contains',
|
|
29
|
+
confidence: 'high',
|
|
30
|
+
});
|
|
31
|
+
};
|
|
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;
|
|
78
|
+
}
|
|
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
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { symbols, dependencies, relationships, warnings };
|
|
98
|
+
}
|
|
@@ -2,8 +2,38 @@ 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 { extractCSymbols } from './c-symbol-extractor.js';
|
|
6
|
+
import { extractCSharpSymbols } from './csharp-symbol-extractor.js';
|
|
5
7
|
import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
|
|
8
|
+
import { extractGoSymbols } from './go-symbol-extractor.js';
|
|
9
|
+
import { extractJvmSymbols } from './jvm-symbol-extractor.js';
|
|
10
|
+
import { extractPythonSymbols } from './python-symbol-extractor.js';
|
|
11
|
+
import { extractRustSymbols } from './rust-symbol-extractor.js';
|
|
6
12
|
import { extractTsSymbols } from './ts-symbol-extractor.js';
|
|
13
|
+
const C_EXTS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx']);
|
|
14
|
+
const JVM_EXTS = new Set(['.java', '.kt', '.kts']);
|
|
15
|
+
function extractSymbols(text, repoPath) {
|
|
16
|
+
const ext = path.extname(repoPath);
|
|
17
|
+
if (ext === '.py' || ext === '.pyw' || ext === '.pyi') {
|
|
18
|
+
return extractPythonSymbols(text, repoPath);
|
|
19
|
+
}
|
|
20
|
+
if (ext === '.go') {
|
|
21
|
+
return extractGoSymbols(text, repoPath);
|
|
22
|
+
}
|
|
23
|
+
if (ext === '.rs') {
|
|
24
|
+
return extractRustSymbols(text, repoPath);
|
|
25
|
+
}
|
|
26
|
+
if (JVM_EXTS.has(ext)) {
|
|
27
|
+
return extractJvmSymbols(text, repoPath);
|
|
28
|
+
}
|
|
29
|
+
if (C_EXTS.has(ext)) {
|
|
30
|
+
return extractCSymbols(text, repoPath);
|
|
31
|
+
}
|
|
32
|
+
if (ext === '.cs') {
|
|
33
|
+
return extractCSharpSymbols(text, repoPath);
|
|
34
|
+
}
|
|
35
|
+
return extractTsSymbols(text, repoPath);
|
|
36
|
+
}
|
|
7
37
|
export async function scanRepository(rootPath, config, previous) {
|
|
8
38
|
const gitignorePatterns = await readGitignorePatterns(rootPath);
|
|
9
39
|
const allExcludes = [...config.exclude, ...gitignorePatterns];
|
|
@@ -47,7 +77,7 @@ export async function scanRepository(rootPath, config, previous) {
|
|
|
47
77
|
warnings: [],
|
|
48
78
|
};
|
|
49
79
|
if (isPreciseLanguage(repoPath, config)) {
|
|
50
|
-
const extracted =
|
|
80
|
+
const extracted = extractSymbols(text, repoPath);
|
|
51
81
|
symbols.push(...extracted.symbols);
|
|
52
82
|
dependencies.push(...extracted.dependencies);
|
|
53
83
|
relationships.push(...extracted.relationships);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export function extractRustSymbols(sourceText, filePath) {
|
|
2
|
+
const lines = sourceText.split('\n');
|
|
3
|
+
const symbols = [];
|
|
4
|
+
const dependencies = [];
|
|
5
|
+
const relationships = [];
|
|
6
|
+
const warnings = [];
|
|
7
|
+
// Track current impl block: { typeName, indent }
|
|
8
|
+
const implStack = [];
|
|
9
|
+
let braceDepth = 0;
|
|
10
|
+
const addSymbol = (name, kind, lineNum, parentName) => {
|
|
11
|
+
const id = [filePath, kind, parentName, name, lineNum]
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.join('#');
|
|
14
|
+
// Rust: pub = exported
|
|
15
|
+
symbols.push({
|
|
16
|
+
id,
|
|
17
|
+
name,
|
|
18
|
+
kind,
|
|
19
|
+
filePath,
|
|
20
|
+
startLine: lineNum,
|
|
21
|
+
endLine: lineNum,
|
|
22
|
+
exported: false, // set by caller
|
|
23
|
+
parentName,
|
|
24
|
+
});
|
|
25
|
+
relationships.push({
|
|
26
|
+
sourceType: 'file',
|
|
27
|
+
sourceId: filePath,
|
|
28
|
+
targetType: 'symbol',
|
|
29
|
+
targetId: id,
|
|
30
|
+
relationshipType: 'contains',
|
|
31
|
+
confidence: 'high',
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const addSymbolExported = (name, kind, lineNum, exported, parentName) => {
|
|
35
|
+
const id = [filePath, kind, parentName, name, lineNum]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join('#');
|
|
38
|
+
symbols.push({
|
|
39
|
+
id,
|
|
40
|
+
name,
|
|
41
|
+
kind,
|
|
42
|
+
filePath,
|
|
43
|
+
startLine: lineNum,
|
|
44
|
+
endLine: lineNum,
|
|
45
|
+
exported,
|
|
46
|
+
parentName,
|
|
47
|
+
});
|
|
48
|
+
relationships.push({
|
|
49
|
+
sourceType: 'file',
|
|
50
|
+
sourceId: filePath,
|
|
51
|
+
targetType: 'symbol',
|
|
52
|
+
targetId: id,
|
|
53
|
+
relationshipType: 'contains',
|
|
54
|
+
confidence: 'high',
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const line = lines[i];
|
|
59
|
+
const lineNum = i + 1;
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
62
|
+
continue;
|
|
63
|
+
// Track brace depth for impl block scoping
|
|
64
|
+
braceDepth +=
|
|
65
|
+
(line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
|
|
66
|
+
// Pop impl blocks we've exited
|
|
67
|
+
while (implStack.length > 0 &&
|
|
68
|
+
braceDepth < implStack[implStack.length - 1].braceDepth) {
|
|
69
|
+
implStack.pop();
|
|
70
|
+
}
|
|
71
|
+
// use statement: use crate::path or use external::path
|
|
72
|
+
const useMatch = trimmed.match(/^use\s+([\w:]+)/);
|
|
73
|
+
if (useMatch) {
|
|
74
|
+
const specifier = useMatch[1];
|
|
75
|
+
const kind = specifier.startsWith('crate::') ||
|
|
76
|
+
specifier.startsWith('super::') ||
|
|
77
|
+
specifier.startsWith('self::')
|
|
78
|
+
? 'local'
|
|
79
|
+
: 'package';
|
|
80
|
+
dependencies.push({ fromFile: filePath, specifier, kind });
|
|
81
|
+
relationships.push({
|
|
82
|
+
sourceType: 'file',
|
|
83
|
+
sourceId: filePath,
|
|
84
|
+
targetType: kind === 'local' ? 'file' : 'package',
|
|
85
|
+
targetId: specifier,
|
|
86
|
+
relationshipType: 'import',
|
|
87
|
+
confidence: 'high',
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// impl block: impl TypeName or impl Trait for TypeName
|
|
92
|
+
const implMatch = trimmed.match(/^impl(?:<[^>]*>)?\s+(?:\w+\s+for\s+)?(\w+)/);
|
|
93
|
+
if (implMatch) {
|
|
94
|
+
implStack.push({ typeName: implMatch[1], braceDepth });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// struct / enum / trait definition
|
|
98
|
+
const typeMatch = trimmed.match(/^(pub\s+)?(?:struct|enum|trait)\s+(\w+)/);
|
|
99
|
+
if (typeMatch) {
|
|
100
|
+
addSymbolExported(typeMatch[2], 'class', lineNum, !!typeMatch[1]);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// fn definition (inside or outside impl)
|
|
104
|
+
const fnMatch = trimmed.match(/^(pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
105
|
+
if (fnMatch) {
|
|
106
|
+
const exported = !!fnMatch[1];
|
|
107
|
+
const name = fnMatch[2];
|
|
108
|
+
const parent = implStack[implStack.length - 1];
|
|
109
|
+
if (parent) {
|
|
110
|
+
addSymbolExported(name, 'method', lineNum, exported, parent.typeName);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
addSymbolExported(name, 'function', lineNum, exported);
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { symbols, dependencies, relationships, warnings };
|
|
119
|
+
}
|