@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 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
- Supported languages: TypeScript, JavaScript, Python, Go, Rust, Java, Kotlin, C/C++, C#, Ruby, PHP, Swift, and 30+ more — detected by file extension, no configuration needed.
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
 
@@ -35,7 +35,35 @@ export const DEFAULT_CONFIG = {
35
35
  '.DS_Store',
36
36
  ],
37
37
  languages: {
38
- precise: ['.js', '.jsx', '.ts', '.tsx'],
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: {},
@@ -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(" ").toLowerCase();
17
+ const haystack = values.join(' ').toLowerCase();
18
18
  for (const token of tokens) {
19
19
  if (haystack.includes(token)) {
20
- score += field.name === "path" || field.name === "name" ? 3 : 1;
21
- reasons.push(`${field.name} matched "${token}"`);
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,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractCSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
@@ -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,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractCSharpSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
@@ -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,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractGoSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
@@ -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,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractJvmSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
@@ -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,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractPythonSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
@@ -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 = extractTsSymbols(text, repoPath);
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,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractRustSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {