@kentwynn/kgraph 0.1.10 → 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
  }
@@ -14,10 +14,27 @@ Before exploring the repository, run \`kgraph context "<topic>"\` to load existi
14
14
  1. Infer the topic from the user's request.
15
15
  2. Run \`kgraph context "<topic>"\`.
16
16
  3. Use the returned files, symbols, relationships, and cognition before broad exploration.
17
- 4. Save durable discoveries to \`.kgraph/inbox/\` and immediately run \`kgraph update\`.
17
+ 4. **After completing the work**, always write a Markdown note to \`.kgraph/inbox/<slug>.md\` capturing what was built or changed, which files and symbols were touched, and any decisions made. Then immediately run \`kgraph update\`.
18
18
  5. If you created, moved, deleted, or renamed files or symbols during this session, run \`kgraph scan\` and report the summary. Skip it otherwise.
19
19
  6. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
20
20
  7. Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
21
+
22
+ The inbox note must use this structure:
23
+ \`\`\`markdown
24
+ # <Short Title>
25
+
26
+ ## Summary
27
+ One or two sentences describing what was done.
28
+
29
+ ## Key Files
30
+ - \`path/to/file.ts\` — what it does
31
+
32
+ ## Key Symbols
33
+ - \`FunctionName\` — what it does
34
+
35
+ ## Decisions
36
+ Any architectural or implementation decisions made.
37
+ \`\`\`
21
38
  `,
22
39
  },
23
40
  {
@@ -21,10 +21,27 @@ Workflow:
21
21
  1. Infer the current topic from the user request.
22
22
  2. Run \`kgraph context "<topic>"\` before broad repo exploration.
23
23
  3. Use KGraph's returned files, symbols, relationships, and cognition as navigation hints.
24
- 4. After durable discoveries, write a concise Markdown note to \`.kgraph/inbox/\` and immediately run \`kgraph update\`.
24
+ 4. **After completing the work**, always write a Markdown note to \`.kgraph/inbox/<slug>.md\` capturing what was built or changed, which files and symbols were touched, and any decisions made. Then immediately run \`kgraph update\`.
25
25
  5. If you created, moved, deleted, or renamed files or symbols during this session, run \`kgraph scan\`. Skip it otherwise.
26
26
  6. Run \`kgraph visualize\` when the user wants to inspect the dependency graph — opens an interactive graph at http://localhost:4242 with PNG export.
27
27
  7. Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
28
+
29
+ The inbox note must use this structure:
30
+ \`\`\`markdown
31
+ # <Short Title>
32
+
33
+ ## Summary
34
+ One or two sentences describing what was done.
35
+
36
+ ## Key Files
37
+ - \`path/to/file.ts\` — what it does
38
+
39
+ ## Key Symbols
40
+ - \`FunctionName\` — what it does
41
+
42
+ ## Decisions
43
+ Any architectural or implementation decisions made.
44
+ \`\`\`
28
45
  `,
29
46
  },
30
47
  ],
@@ -4,11 +4,26 @@ export const copilotAdapter = {
4
4
  targetPath: '.github/copilot-instructions.md',
5
5
  instructions: `## KGraph Workflow
6
6
 
7
- 1. Before exploring the repository, run \`kgraph context "<topic>"\` to load existing repo intelligence.
8
- 2. Use the returned files, symbols, relationships, and cognition before broad exploration.
9
- 3. After durable discoveries (architecture decisions, debugging insights, structural findings), write a concise Markdown note to \`.kgraph/inbox/\` and immediately run \`kgraph update\`.
10
- 4. If you created, moved, deleted, or renamed files or symbols, run \`kgraph scan\`.
11
- 5. Use /kgraph-scan, /kgraph-update, /kgraph-visualize, and /kgraph-history for manual maintenance steps.
7
+ 1. **Before exploring the repository**, run \`kgraph context "<topic>"\` to load existing repo intelligence. Use the returned files, symbols, and cognition before any broad exploration.
8
+ 2. **After every meaningful implementation or discovery session**, write a Markdown note to \`.kgraph/inbox/<slug>.md\` capturing what was built, which files were touched, which symbols matter, and any decisions made. Then immediately run \`kgraph update\`.
9
+ 3. If you created, moved, deleted, or renamed files or symbols, run \`kgraph scan\`.
10
+
11
+ The inbox note must use this structure:
12
+ \`\`\`markdown
13
+ # <Short Title>
14
+
15
+ ## Summary
16
+ One or two sentences describing what was done.
17
+
18
+ ## Key Files
19
+ - \`path/to/file.ts\` — what it does
20
+
21
+ ## Key Symbols
22
+ - \`FunctionName\` — what it does
23
+
24
+ ## Decisions
25
+ Any architectural or implementation decisions made.
26
+ \`\`\`
12
27
  `,
13
28
  commandFiles: [
14
29
  {
@@ -39,6 +54,38 @@ agent: agent
39
54
  ---
40
55
 
41
56
  Run \`kgraph visualize\` to start the interactive dependency graph at http://localhost:4242, then summarize what nodes and connections are visible.
57
+ `,
58
+ },
59
+ {
60
+ path: '.github/prompts/kgraph-capture.prompt.md',
61
+ content: `---
62
+ description: Save what was just built or changed into KGraph cognition
63
+ agent: agent
64
+ argument-hint: "Brief description of what was done"
65
+ ---
66
+
67
+ Capture this session into KGraph cognition.
68
+
69
+ 1. Write a Markdown note to \`.kgraph/inbox/<slug>.md\` using the structure below. Use the user's message as the summary if provided.
70
+ 2. Immediately run \`kgraph update\`.
71
+ 3. If files or symbols were created, moved, deleted, or renamed, also run \`kgraph scan\`.
72
+
73
+ Note structure:
74
+ \`\`\`markdown
75
+ # <Short Title>
76
+
77
+ ## Summary
78
+ One or two sentences describing what was done.
79
+
80
+ ## Key Files
81
+ - \`path/to/file.ts\` — what it does
82
+
83
+ ## Key Symbols
84
+ - \`FunctionName\` — what it does
85
+
86
+ ## Decisions
87
+ Any architectural or implementation decisions made.
88
+ \`\`\`
42
89
  `,
43
90
  },
44
91
  {
@@ -9,8 +9,8 @@ alwaysApply: true
9
9
 
10
10
  ## KGraph Workflow
11
11
 
12
- - Before exploring the repository, run \`kgraph context "<topic>"\` to load existing repo intelligence.
13
- - After durable discoveries (architecture decisions, debugging insights, structural findings), write a concise Markdown note to \`.kgraph/inbox/\` and immediately run \`kgraph update\`.
12
+ - **Before exploring the repository**, run \`kgraph context "<topic>"\` to load existing repo intelligence. Use the returned files, symbols, and cognition before any broad exploration.
13
+ - **After every meaningful implementation or discovery session**, write a Markdown note to \`.kgraph/inbox/<slug>.md\` (title, Key Files, Key Symbols, Decisions sections) and immediately run \`kgraph update\`.
14
14
  - If you created, moved, deleted, or renamed files or symbols, run \`kgraph scan\`.
15
15
  - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
16
16
  - Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
@@ -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.10",
3
+ "version": "0.1.12",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {