@kentwynn/kgraph 0.2.21 → 0.2.23
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 +71 -14
- package/dist/cli/help.js +3 -2
- package/dist/cli/init-summary.d.ts +1 -1
- package/dist/cli/init-summary.js +28 -8
- package/dist/config/config.js +41 -0
- package/dist/scanner/broad-symbol-extractor.d.ts +3 -0
- package/dist/scanner/broad-symbol-extractor.js +292 -0
- package/dist/scanner/extraction-context.d.ts +23 -0
- package/dist/scanner/extraction-context.js +77 -0
- package/dist/scanner/file-classifier.js +15 -2
- package/dist/scanner/php-symbol-extractor.d.ts +2 -0
- package/dist/scanner/php-symbol-extractor.js +79 -0
- package/dist/scanner/repo-scanner.js +35 -1
- package/dist/scanner/ruby-symbol-extractor.d.ts +2 -0
- package/dist/scanner/ruby-symbol-extractor.js +75 -0
- package/dist/scanner/shell-symbol-extractor.d.ts +2 -0
- package/dist/scanner/shell-symbol-extractor.js +78 -0
- package/dist/scanner/sql-symbol-extractor.d.ts +2 -0
- package/dist/scanner/sql-symbol-extractor.js +166 -0
- package/dist/scanner/tree-sitter-parser.d.ts +1 -2
- package/dist/scanner/tree-sitter-parser.js +14 -0
- package/media/logo.svg +29 -0
- package/package.json +13 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export class ExtractionContext {
|
|
2
|
+
filePath;
|
|
3
|
+
symbols = [];
|
|
4
|
+
dependencies = [];
|
|
5
|
+
relationships = [];
|
|
6
|
+
warnings = [];
|
|
7
|
+
constructor(filePath) {
|
|
8
|
+
this.filePath = filePath;
|
|
9
|
+
}
|
|
10
|
+
addSymbol(options) {
|
|
11
|
+
const id = [
|
|
12
|
+
this.filePath,
|
|
13
|
+
options.kind,
|
|
14
|
+
options.parentName,
|
|
15
|
+
options.name,
|
|
16
|
+
options.startLine,
|
|
17
|
+
options.endLine ?? options.startLine,
|
|
18
|
+
]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join('#');
|
|
21
|
+
const symbol = {
|
|
22
|
+
id,
|
|
23
|
+
name: options.name,
|
|
24
|
+
kind: options.kind,
|
|
25
|
+
filePath: this.filePath,
|
|
26
|
+
startLine: options.startLine,
|
|
27
|
+
endLine: options.endLine ?? options.startLine,
|
|
28
|
+
exported: options.exported ?? false,
|
|
29
|
+
parentName: options.parentName,
|
|
30
|
+
};
|
|
31
|
+
this.symbols.push(symbol);
|
|
32
|
+
this.relationships.push({
|
|
33
|
+
sourceType: 'file',
|
|
34
|
+
sourceId: this.filePath,
|
|
35
|
+
targetType: 'symbol',
|
|
36
|
+
targetId: id,
|
|
37
|
+
relationshipType: 'contains',
|
|
38
|
+
confidence: 'high',
|
|
39
|
+
});
|
|
40
|
+
return symbol;
|
|
41
|
+
}
|
|
42
|
+
addDependency(specifier, kind = specifier.startsWith('.') ? 'local' : 'package', confidence = 'high') {
|
|
43
|
+
this.dependencies.push({ fromFile: this.filePath, specifier, kind });
|
|
44
|
+
this.relationships.push({
|
|
45
|
+
sourceType: 'file',
|
|
46
|
+
sourceId: this.filePath,
|
|
47
|
+
targetType: kind === 'local' ? 'file' : 'package',
|
|
48
|
+
targetId: specifier,
|
|
49
|
+
relationshipType: 'import',
|
|
50
|
+
confidence,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
addSymbolContains(parent, child) {
|
|
54
|
+
this.relationships.push({
|
|
55
|
+
sourceType: 'symbol',
|
|
56
|
+
sourceId: parent.id,
|
|
57
|
+
targetType: 'symbol',
|
|
58
|
+
targetId: child.id,
|
|
59
|
+
relationshipType: 'symbol-contains',
|
|
60
|
+
confidence: 'high',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
addWarning(message) {
|
|
64
|
+
this.warnings.push(message);
|
|
65
|
+
}
|
|
66
|
+
toResult() {
|
|
67
|
+
return {
|
|
68
|
+
symbols: this.symbols,
|
|
69
|
+
dependencies: this.dependencies,
|
|
70
|
+
relationships: this.relationships,
|
|
71
|
+
warnings: this.warnings,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function emptyExtractionResult() {
|
|
76
|
+
return { symbols: [], dependencies: [], relationships: [], warnings: [] };
|
|
77
|
+
}
|
|
@@ -53,6 +53,7 @@ const LANGUAGE_BY_EXTENSION = {
|
|
|
53
53
|
'.scss': 'scss',
|
|
54
54
|
'.sass': 'sass',
|
|
55
55
|
'.less': 'less',
|
|
56
|
+
'.dockerfile': 'dockerfile',
|
|
56
57
|
'.vue': 'vue',
|
|
57
58
|
'.svelte': 'svelte',
|
|
58
59
|
// Data / Config
|
|
@@ -84,6 +85,13 @@ const LANGUAGE_BY_EXTENSION = {
|
|
|
84
85
|
'.proto': 'protobuf',
|
|
85
86
|
'.sql': 'sql',
|
|
86
87
|
};
|
|
88
|
+
const LANGUAGE_BY_BASENAME = {
|
|
89
|
+
Dockerfile: 'dockerfile',
|
|
90
|
+
Containerfile: 'dockerfile',
|
|
91
|
+
Makefile: 'shell',
|
|
92
|
+
Rakefile: 'ruby',
|
|
93
|
+
Gemfile: 'ruby',
|
|
94
|
+
};
|
|
87
95
|
export function shouldExclude(repoPath, config) {
|
|
88
96
|
const normalizedPath = normalizeRepoPath(repoPath);
|
|
89
97
|
return config.exclude.some((pattern) => matchesExcludePattern(normalizedPath, pattern));
|
|
@@ -120,10 +128,15 @@ export async function readGitignorePatterns(rootPath) {
|
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
export function detectLanguage(filePath) {
|
|
123
|
-
|
|
131
|
+
const basename = path.basename(filePath);
|
|
132
|
+
return (LANGUAGE_BY_BASENAME[basename] ??
|
|
133
|
+
LANGUAGE_BY_EXTENSION[path.extname(filePath)] ??
|
|
134
|
+
'unknown');
|
|
124
135
|
}
|
|
125
136
|
export function isPreciseLanguage(filePath, config) {
|
|
126
|
-
|
|
137
|
+
const basename = path.basename(filePath);
|
|
138
|
+
return (config.languages.precise.includes(path.extname(filePath)) ||
|
|
139
|
+
Object.hasOwn(LANGUAGE_BY_BASENAME, basename));
|
|
127
140
|
}
|
|
128
141
|
function matchesExcludePattern(repoPath, pattern) {
|
|
129
142
|
const normalized = normalizeRepoPath(pattern).replace(/\/$/, '');
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
|
|
2
|
+
import { parseSource } from './tree-sitter-parser.js';
|
|
3
|
+
export async function extractPhpSymbols(sourceText, filePath) {
|
|
4
|
+
if (!sourceText.trim()) {
|
|
5
|
+
return emptyExtractionResult();
|
|
6
|
+
}
|
|
7
|
+
const tree = await parseSource(sourceText, 'php');
|
|
8
|
+
const context = new ExtractionContext(filePath);
|
|
9
|
+
function addNamedSymbol(node, kind, parentName) {
|
|
10
|
+
const nameNode = findNameNode(node);
|
|
11
|
+
if (!nameNode)
|
|
12
|
+
return undefined;
|
|
13
|
+
return context.addSymbol({
|
|
14
|
+
name: nameNode.text,
|
|
15
|
+
kind,
|
|
16
|
+
startLine: node.startPosition.row + 1,
|
|
17
|
+
endLine: node.endPosition.row + 1,
|
|
18
|
+
exported: true,
|
|
19
|
+
parentName,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function walk(node, parentClassName) {
|
|
23
|
+
switch (node.type) {
|
|
24
|
+
case 'namespace_definition':
|
|
25
|
+
case 'namespace_name': {
|
|
26
|
+
if (node.type === 'namespace_name' && !parentClassName) {
|
|
27
|
+
context.addSymbol({
|
|
28
|
+
name: node.text,
|
|
29
|
+
kind: 'type',
|
|
30
|
+
startLine: node.startPosition.row + 1,
|
|
31
|
+
endLine: node.endPosition.row + 1,
|
|
32
|
+
exported: true,
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case 'namespace_use_declaration': {
|
|
39
|
+
for (const nameNode of node.descendantsOfType('qualified_name')) {
|
|
40
|
+
context.addDependency(nameNode.text, 'package');
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
case 'class_declaration':
|
|
45
|
+
case 'interface_declaration':
|
|
46
|
+
case 'trait_declaration':
|
|
47
|
+
case 'enum_declaration': {
|
|
48
|
+
const classSymbol = addNamedSymbol(node, node.type === 'interface_declaration' ? 'interface' : 'class', parentClassName);
|
|
49
|
+
const className = classSymbol?.name ?? parentClassName;
|
|
50
|
+
for (const child of node.namedChildren) {
|
|
51
|
+
walk(child, className);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case 'function_definition':
|
|
56
|
+
case 'method_declaration': {
|
|
57
|
+
const symbol = addNamedSymbol(node, parentClassName ? 'method' : 'function', parentClassName);
|
|
58
|
+
if (symbol && parentClassName) {
|
|
59
|
+
const parent = context.symbols.find((candidate) => candidate.name === parentClassName && candidate.kind === 'class');
|
|
60
|
+
if (parent)
|
|
61
|
+
context.addSymbolContains(parent, symbol);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const child of node.namedChildren) {
|
|
67
|
+
walk(child, parentClassName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(tree.rootNode);
|
|
71
|
+
tree.delete();
|
|
72
|
+
return context.toResult();
|
|
73
|
+
}
|
|
74
|
+
function findNameNode(node) {
|
|
75
|
+
return (node.childForFieldName('name') ??
|
|
76
|
+
node.namedChildren.find((child) => child.type === 'name') ??
|
|
77
|
+
node.namedChildren.find((child) => child.type === 'variable_name') ??
|
|
78
|
+
node.namedChildren.find((child) => child.type === 'identifier'));
|
|
79
|
+
}
|
|
@@ -3,22 +3,44 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import { readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { estimateTokens } from '../session/token-estimator.js';
|
|
6
|
+
import { extractBroadSymbols, supportsBroadExtraction, } from './broad-symbol-extractor.js';
|
|
6
7
|
import { extractCSymbols } from './c-symbol-extractor.js';
|
|
7
8
|
import { extractCSharpSymbols } from './csharp-symbol-extractor.js';
|
|
8
9
|
import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
|
|
9
10
|
import { getChangedFilesSince, isGitRepo } from './git-utils.js';
|
|
10
11
|
import { extractGoSymbols } from './go-symbol-extractor.js';
|
|
11
12
|
import { extractJvmSymbols } from './jvm-symbol-extractor.js';
|
|
13
|
+
import { extractPhpSymbols } from './php-symbol-extractor.js';
|
|
12
14
|
import { extractPythonSymbols } from './python-symbol-extractor.js';
|
|
15
|
+
import { extractRubySymbols } from './ruby-symbol-extractor.js';
|
|
13
16
|
import { extractRustSymbols } from './rust-symbol-extractor.js';
|
|
17
|
+
import { extractShellSymbols } from './shell-symbol-extractor.js';
|
|
18
|
+
import { extractSqlSymbols } from './sql-symbol-extractor.js';
|
|
14
19
|
import { extractTsSymbols } from './ts-symbol-extractor.js';
|
|
15
20
|
const C_EXTS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx']);
|
|
16
21
|
const JVM_EXTS = new Set(['.java', '.kt', '.kts']);
|
|
22
|
+
const TS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
17
23
|
async function extractSymbols(text, repoPath) {
|
|
18
24
|
const ext = path.extname(repoPath);
|
|
25
|
+
const language = detectLanguage(repoPath);
|
|
26
|
+
if (supportsBroadExtraction(language)) {
|
|
27
|
+
return extractBroadSymbols(text, repoPath, language);
|
|
28
|
+
}
|
|
19
29
|
if (ext === '.py' || ext === '.pyw' || ext === '.pyi') {
|
|
20
30
|
return extractPythonSymbols(text, repoPath);
|
|
21
31
|
}
|
|
32
|
+
if (ext === '.php') {
|
|
33
|
+
return extractPhpSymbols(text, repoPath);
|
|
34
|
+
}
|
|
35
|
+
if (ext === '.rb' || ext === '.rake' || language === 'ruby') {
|
|
36
|
+
return extractRubySymbols(text, repoPath);
|
|
37
|
+
}
|
|
38
|
+
if (['.sh', '.bash', '.zsh', '.fish'].includes(ext) || language === 'shell') {
|
|
39
|
+
return extractShellSymbols(text, repoPath);
|
|
40
|
+
}
|
|
41
|
+
if (ext === '.sql') {
|
|
42
|
+
return extractSqlSymbols(text, repoPath);
|
|
43
|
+
}
|
|
22
44
|
if (ext === '.go') {
|
|
23
45
|
return extractGoSymbols(text, repoPath);
|
|
24
46
|
}
|
|
@@ -34,7 +56,10 @@ async function extractSymbols(text, repoPath) {
|
|
|
34
56
|
if (ext === '.cs') {
|
|
35
57
|
return extractCSharpSymbols(text, repoPath);
|
|
36
58
|
}
|
|
37
|
-
|
|
59
|
+
if (TS_EXTS.has(ext)) {
|
|
60
|
+
return extractTsSymbols(text, repoPath);
|
|
61
|
+
}
|
|
62
|
+
return { symbols: [], dependencies: [], relationships: [], warnings: [] };
|
|
38
63
|
}
|
|
39
64
|
export async function scanRepository(rootPath, config, previous) {
|
|
40
65
|
const gitignorePatterns = await readGitignorePatterns(rootPath);
|
|
@@ -223,6 +248,15 @@ const SOURCE_EXTENSIONS = [
|
|
|
223
248
|
'.hpp',
|
|
224
249
|
'.hxx',
|
|
225
250
|
'.cs',
|
|
251
|
+
'.php',
|
|
252
|
+
'.swift',
|
|
253
|
+
'.rb',
|
|
254
|
+
'.rake',
|
|
255
|
+
'.lua',
|
|
256
|
+
'.dart',
|
|
257
|
+
'.ex',
|
|
258
|
+
'.exs',
|
|
259
|
+
'.scala',
|
|
226
260
|
];
|
|
227
261
|
function resolveLocalDependencies(dependencies, files) {
|
|
228
262
|
const filePaths = new Set(files.map((file) => file.path));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
|
|
2
|
+
import { parseSource } from './tree-sitter-parser.js';
|
|
3
|
+
export async function extractRubySymbols(sourceText, filePath) {
|
|
4
|
+
if (!sourceText.trim()) {
|
|
5
|
+
return emptyExtractionResult();
|
|
6
|
+
}
|
|
7
|
+
const tree = await parseSource(sourceText, 'ruby');
|
|
8
|
+
const context = new ExtractionContext(filePath);
|
|
9
|
+
function addNamedSymbol(node, kind, parentName) {
|
|
10
|
+
const nameNode = findNameNode(node);
|
|
11
|
+
if (!nameNode)
|
|
12
|
+
return undefined;
|
|
13
|
+
return context.addSymbol({
|
|
14
|
+
name: nameNode.text,
|
|
15
|
+
kind,
|
|
16
|
+
startLine: node.startPosition.row + 1,
|
|
17
|
+
endLine: node.endPosition.row + 1,
|
|
18
|
+
exported: true,
|
|
19
|
+
parentName,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function walk(node, parentName) {
|
|
23
|
+
switch (node.type) {
|
|
24
|
+
case 'call': {
|
|
25
|
+
const methodName = findCallMethodName(node);
|
|
26
|
+
if (methodName === 'require' || methodName === 'require_relative') {
|
|
27
|
+
const argument = findFirstStringContent(node);
|
|
28
|
+
if (argument) {
|
|
29
|
+
context.addDependency(argument, methodName === 'require_relative' ? 'local' : 'package');
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'class':
|
|
36
|
+
case 'module': {
|
|
37
|
+
const symbol = addNamedSymbol(node, node.type === 'class' ? 'class' : 'type', parentName);
|
|
38
|
+
const nextParent = symbol?.name ?? parentName;
|
|
39
|
+
for (const child of node.namedChildren) {
|
|
40
|
+
walk(child, nextParent);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
case 'method':
|
|
45
|
+
case 'singleton_method': {
|
|
46
|
+
const symbol = addNamedSymbol(node, parentName ? 'method' : 'function', parentName);
|
|
47
|
+
if (symbol && parentName) {
|
|
48
|
+
const parent = context.symbols.find((candidate) => candidate.name === parentName &&
|
|
49
|
+
(candidate.kind === 'class' || candidate.kind === 'type'));
|
|
50
|
+
if (parent)
|
|
51
|
+
context.addSymbolContains(parent, symbol);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const child of node.namedChildren) {
|
|
57
|
+
walk(child, parentName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
walk(tree.rootNode);
|
|
61
|
+
tree.delete();
|
|
62
|
+
return context.toResult();
|
|
63
|
+
}
|
|
64
|
+
function findNameNode(node) {
|
|
65
|
+
return (node.childForFieldName('name') ??
|
|
66
|
+
node.namedChildren.find((child) => child.type === 'constant') ??
|
|
67
|
+
node.namedChildren.find((child) => child.type === 'identifier'));
|
|
68
|
+
}
|
|
69
|
+
function findCallMethodName(node) {
|
|
70
|
+
return (node.childForFieldName('method')?.text ??
|
|
71
|
+
node.namedChildren.find((child) => child.type === 'identifier')?.text);
|
|
72
|
+
}
|
|
73
|
+
function findFirstStringContent(node) {
|
|
74
|
+
return node.descendantsOfType('string_content')[0]?.text;
|
|
75
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
|
|
2
|
+
import { parseSource } from './tree-sitter-parser.js';
|
|
3
|
+
export async function extractShellSymbols(sourceText, filePath) {
|
|
4
|
+
if (!sourceText.trim()) {
|
|
5
|
+
return emptyExtractionResult();
|
|
6
|
+
}
|
|
7
|
+
const tree = await parseSource(sourceText, 'bash');
|
|
8
|
+
const context = new ExtractionContext(filePath);
|
|
9
|
+
function walk(node, currentFunctionId) {
|
|
10
|
+
switch (node.type) {
|
|
11
|
+
case 'function_definition': {
|
|
12
|
+
const name = findFunctionName(node);
|
|
13
|
+
if (name) {
|
|
14
|
+
const symbol = context.addSymbol({
|
|
15
|
+
name,
|
|
16
|
+
kind: 'function',
|
|
17
|
+
startLine: node.startPosition.row + 1,
|
|
18
|
+
endLine: node.endPosition.row + 1,
|
|
19
|
+
exported: true,
|
|
20
|
+
});
|
|
21
|
+
for (const child of node.namedChildren) {
|
|
22
|
+
walk(child, symbol.id);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case 'command': {
|
|
29
|
+
const commandName = findCommandName(node);
|
|
30
|
+
const firstArgument = findFirstCommandArgument(node);
|
|
31
|
+
if ((commandName === 'source' || commandName === '.') &&
|
|
32
|
+
firstArgument) {
|
|
33
|
+
context.addDependency(firstArgument, 'local');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (currentFunctionId && commandName && isLocalScriptReference(commandName)) {
|
|
37
|
+
context.relationships.push({
|
|
38
|
+
sourceType: 'symbol',
|
|
39
|
+
sourceId: currentFunctionId,
|
|
40
|
+
targetType: 'file',
|
|
41
|
+
targetId: commandName,
|
|
42
|
+
relationshipType: 'calls',
|
|
43
|
+
confidence: 'low',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const child of node.namedChildren) {
|
|
50
|
+
walk(child, currentFunctionId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
walk(tree.rootNode);
|
|
54
|
+
tree.delete();
|
|
55
|
+
return context.toResult();
|
|
56
|
+
}
|
|
57
|
+
function findFunctionName(node) {
|
|
58
|
+
return (node.childForFieldName('name')?.text ??
|
|
59
|
+
node.namedChildren.find((child) => child.type === 'word')?.text);
|
|
60
|
+
}
|
|
61
|
+
function findCommandName(node) {
|
|
62
|
+
return (node.childForFieldName('name')?.text ??
|
|
63
|
+
node.namedChildren
|
|
64
|
+
.find((child) => child.type === 'command_name')
|
|
65
|
+
?.namedChildren.find((child) => child.type === 'word')?.text ??
|
|
66
|
+
node.namedChildren.find((child) => child.type === 'command_name')?.text ??
|
|
67
|
+
node.namedChildren.find((child) => child.type === 'word')?.text);
|
|
68
|
+
}
|
|
69
|
+
function findFirstCommandArgument(node) {
|
|
70
|
+
return node.namedChildren.find((child) => child.type === 'word')?.text;
|
|
71
|
+
}
|
|
72
|
+
function isLocalScriptReference(value) {
|
|
73
|
+
return (value.startsWith('./') ||
|
|
74
|
+
value.startsWith('../') ||
|
|
75
|
+
value.endsWith('.sh') ||
|
|
76
|
+
value.endsWith('.bash') ||
|
|
77
|
+
value.endsWith('.zsh'));
|
|
78
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
|
|
2
|
+
export async function extractSqlSymbols(sourceText, filePath) {
|
|
3
|
+
if (!sourceText.trim()) {
|
|
4
|
+
return emptyExtractionResult();
|
|
5
|
+
}
|
|
6
|
+
const context = new ExtractionContext(filePath);
|
|
7
|
+
const withoutComments = stripLineComments(sourceText);
|
|
8
|
+
const statements = splitStatements(withoutComments);
|
|
9
|
+
for (const statement of statements) {
|
|
10
|
+
collectCreateStatement(statement, context);
|
|
11
|
+
collectAlterStatement(statement, context);
|
|
12
|
+
collectQueryReferences(statement, context);
|
|
13
|
+
}
|
|
14
|
+
return context.toResult();
|
|
15
|
+
}
|
|
16
|
+
function collectCreateStatement(statement, context) {
|
|
17
|
+
const normalized = compact(statement.text);
|
|
18
|
+
const createMatch = normalized.match(/^CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:UNIQUE|MATERIALIZED|TEMP|TEMPORARY)\s+)*(TABLE|VIEW|INDEX|FUNCTION|PROCEDURE|TRIGGER|TYPE)\s+(?:IF\s+NOT\s+EXISTS\s+)?("?[\w.]+"?)/i);
|
|
19
|
+
if (!createMatch?.[1] || !createMatch[2]) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const objectKind = createMatch[1].toLowerCase();
|
|
23
|
+
const name = normalizeSqlIdentifier(createMatch[2]);
|
|
24
|
+
const symbol = context.addSymbol({
|
|
25
|
+
name,
|
|
26
|
+
kind: objectKind === 'function' || objectKind === 'procedure'
|
|
27
|
+
? 'function'
|
|
28
|
+
: 'type',
|
|
29
|
+
startLine: statement.startLine,
|
|
30
|
+
endLine: statement.endLine,
|
|
31
|
+
exported: true,
|
|
32
|
+
parentName: objectKind,
|
|
33
|
+
});
|
|
34
|
+
const tableForTrigger = normalized.match(/\bON\s+("?[\w.]+"?)/i)?.[1];
|
|
35
|
+
if (objectKind === 'trigger' && tableForTrigger) {
|
|
36
|
+
context.relationships.push({
|
|
37
|
+
sourceType: 'symbol',
|
|
38
|
+
sourceId: symbol.id,
|
|
39
|
+
targetType: 'symbol',
|
|
40
|
+
targetId: normalizeSqlIdentifier(tableForTrigger),
|
|
41
|
+
relationshipType: 'mentions',
|
|
42
|
+
confidence: 'high',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
for (const referencedTable of referencedTables(normalized)) {
|
|
46
|
+
context.relationships.push({
|
|
47
|
+
sourceType: 'symbol',
|
|
48
|
+
sourceId: symbol.id,
|
|
49
|
+
targetType: 'symbol',
|
|
50
|
+
targetId: referencedTable,
|
|
51
|
+
relationshipType: 'mentions',
|
|
52
|
+
confidence: 'medium',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function collectAlterStatement(statement, context) {
|
|
57
|
+
const normalized = compact(statement.text);
|
|
58
|
+
const alterMatch = normalized.match(/^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?("?[\w.]+"?)/i);
|
|
59
|
+
if (!alterMatch?.[1]) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const tableName = normalizeSqlIdentifier(alterMatch[1]);
|
|
63
|
+
const symbol = context.addSymbol({
|
|
64
|
+
name: `alter ${tableName}`,
|
|
65
|
+
kind: 'type',
|
|
66
|
+
startLine: statement.startLine,
|
|
67
|
+
endLine: statement.endLine,
|
|
68
|
+
exported: true,
|
|
69
|
+
parentName: 'table',
|
|
70
|
+
});
|
|
71
|
+
context.relationships.push({
|
|
72
|
+
sourceType: 'symbol',
|
|
73
|
+
sourceId: symbol.id,
|
|
74
|
+
targetType: 'symbol',
|
|
75
|
+
targetId: tableName,
|
|
76
|
+
relationshipType: 'mentions',
|
|
77
|
+
confidence: 'high',
|
|
78
|
+
});
|
|
79
|
+
for (const referencedTable of referencedTables(normalized)) {
|
|
80
|
+
context.relationships.push({
|
|
81
|
+
sourceType: 'symbol',
|
|
82
|
+
sourceId: symbol.id,
|
|
83
|
+
targetType: 'symbol',
|
|
84
|
+
targetId: referencedTable,
|
|
85
|
+
relationshipType: 'mentions',
|
|
86
|
+
confidence: 'medium',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function collectQueryReferences(statement, context) {
|
|
91
|
+
const normalized = compact(statement.text);
|
|
92
|
+
const queryMatch = normalized.match(/^(SELECT|INSERT|UPDATE|DELETE)\b/i);
|
|
93
|
+
if (!queryMatch?.[1]) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const symbol = context.addSymbol({
|
|
97
|
+
name: `${queryMatch[1].toLowerCase()} statement ${statement.startLine}`,
|
|
98
|
+
kind: 'type',
|
|
99
|
+
startLine: statement.startLine,
|
|
100
|
+
endLine: statement.endLine,
|
|
101
|
+
});
|
|
102
|
+
for (const table of referencedTables(normalized)) {
|
|
103
|
+
context.relationships.push({
|
|
104
|
+
sourceType: 'symbol',
|
|
105
|
+
sourceId: symbol.id,
|
|
106
|
+
targetType: 'symbol',
|
|
107
|
+
targetId: table,
|
|
108
|
+
relationshipType: 'mentions',
|
|
109
|
+
confidence: 'medium',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function stripLineComments(sourceText) {
|
|
114
|
+
return sourceText
|
|
115
|
+
.split(/\r?\n/)
|
|
116
|
+
.map((line) => line.replace(/--.*$/, ''))
|
|
117
|
+
.join('\n');
|
|
118
|
+
}
|
|
119
|
+
function splitStatements(sourceText) {
|
|
120
|
+
const statements = [];
|
|
121
|
+
let current = '';
|
|
122
|
+
let startLine = 1;
|
|
123
|
+
let lineNumber = 1;
|
|
124
|
+
for (const line of sourceText.split(/\r?\n/)) {
|
|
125
|
+
if (!current.trim()) {
|
|
126
|
+
startLine = lineNumber;
|
|
127
|
+
}
|
|
128
|
+
current += `${line}\n`;
|
|
129
|
+
if (line.includes(';')) {
|
|
130
|
+
statements.push({
|
|
131
|
+
text: current,
|
|
132
|
+
startLine,
|
|
133
|
+
endLine: lineNumber,
|
|
134
|
+
});
|
|
135
|
+
current = '';
|
|
136
|
+
}
|
|
137
|
+
lineNumber += 1;
|
|
138
|
+
}
|
|
139
|
+
if (current.trim()) {
|
|
140
|
+
statements.push({ text: current, startLine, endLine: lineNumber - 1 });
|
|
141
|
+
}
|
|
142
|
+
return statements;
|
|
143
|
+
}
|
|
144
|
+
function compact(value) {
|
|
145
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
146
|
+
}
|
|
147
|
+
function normalizeSqlIdentifier(value) {
|
|
148
|
+
return value.replace(/^"+|"+$/g, '').replace(/[;,)]$/, '');
|
|
149
|
+
}
|
|
150
|
+
function referencedTables(statement) {
|
|
151
|
+
const names = new Set();
|
|
152
|
+
const patterns = [
|
|
153
|
+
/\bREFERENCES\s+("?[\w.]+"?)/gi,
|
|
154
|
+
/\bFROM\s+("?[\w.]+"?)/gi,
|
|
155
|
+
/\bJOIN\s+("?[\w.]+"?)/gi,
|
|
156
|
+
/\bUPDATE\s+("?[\w.]+"?)/gi,
|
|
157
|
+
/\bINTO\s+("?[\w.]+"?)/gi,
|
|
158
|
+
];
|
|
159
|
+
for (const pattern of patterns) {
|
|
160
|
+
for (const match of statement.matchAll(pattern)) {
|
|
161
|
+
if (match[1])
|
|
162
|
+
names.add(normalizeSqlIdentifier(match[1]));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return [...names];
|
|
166
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Language, Tree } from 'web-tree-sitter';
|
|
2
|
-
type GrammarKey = 'python' | 'java' | 'kotlin' | 'go' | 'rust' | 'c' | 'cpp' | 'c_sharp';
|
|
2
|
+
export type GrammarKey = 'python' | 'java' | 'kotlin' | 'go' | 'rust' | 'c' | 'cpp' | 'c_sharp' | 'php' | 'ruby' | 'bash' | 'yaml' | 'json' | 'html' | 'css' | 'lua' | 'dart' | 'elixir' | 'scala';
|
|
3
3
|
export declare function loadLanguage(grammarKey: GrammarKey): Promise<Language>;
|
|
4
4
|
export declare function parseSource(sourceText: string, grammarKey: GrammarKey): Promise<Tree>;
|
|
5
|
-
export {};
|
|
@@ -19,6 +19,20 @@ const GRAMMAR_PACKAGES = {
|
|
|
19
19
|
pkg: 'tree-sitter-c-sharp',
|
|
20
20
|
wasm: 'tree-sitter-c_sharp.wasm',
|
|
21
21
|
},
|
|
22
|
+
php: { pkg: 'tree-sitter-php', wasm: 'tree-sitter-php.wasm' },
|
|
23
|
+
ruby: { pkg: 'tree-sitter-ruby', wasm: 'tree-sitter-ruby.wasm' },
|
|
24
|
+
bash: { pkg: 'tree-sitter-bash', wasm: 'tree-sitter-bash.wasm' },
|
|
25
|
+
yaml: {
|
|
26
|
+
pkg: '@tree-sitter-grammars/tree-sitter-yaml',
|
|
27
|
+
wasm: 'tree-sitter-yaml.wasm',
|
|
28
|
+
},
|
|
29
|
+
json: { pkg: 'tree-sitter-json', wasm: 'tree-sitter-json.wasm' },
|
|
30
|
+
html: { pkg: 'tree-sitter-html', wasm: 'tree-sitter-html.wasm' },
|
|
31
|
+
css: { pkg: 'tree-sitter-css', wasm: 'tree-sitter-css.wasm' },
|
|
32
|
+
lua: { pkg: 'tree-sitter-lua', wasm: 'tree-sitter-lua.wasm' },
|
|
33
|
+
dart: { pkg: 'tree-sitter-dart', wasm: 'tree-sitter-dart.wasm' },
|
|
34
|
+
elixir: { pkg: 'tree-sitter-elixir', wasm: 'tree-sitter-elixir.wasm' },
|
|
35
|
+
scala: { pkg: 'tree-sitter-scala', wasm: 'tree-sitter-scala.wasm' },
|
|
22
36
|
};
|
|
23
37
|
async function ensureInit() {
|
|
24
38
|
if (!initPromise) {
|
package/media/logo.svg
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">KGraph Atom Core Logo</title>
|
|
3
|
+
<desc id="desc">Atom Core logo for KGraph, persistent repository intelligence for AI coding tools.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<radialGradient id="core" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(110 104) rotate(90) scale(88)">
|
|
6
|
+
<stop offset="0" stop-color="#22D3EE"/>
|
|
7
|
+
<stop offset="0.58" stop-color="#38BDF8"/>
|
|
8
|
+
<stop offset="1" stop-color="#A78BFA"/>
|
|
9
|
+
</radialGradient>
|
|
10
|
+
<linearGradient id="orbit" x1="36" y1="56" x2="184" y2="164" gradientUnits="userSpaceOnUse">
|
|
11
|
+
<stop stop-color="#22D3EE"/>
|
|
12
|
+
<stop offset="1" stop-color="#C084FC"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<filter id="glow" x="12" y="12" width="196" height="196" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
15
|
+
<feGaussianBlur stdDeviation="6" result="blur"/>
|
|
16
|
+
<feColorMatrix in="blur" type="matrix" values="0 0 0 0 0.133 0 0 0 0 0.827 0 0 0 0 0.933 0 0 0 0.42 0"/>
|
|
17
|
+
<feBlend in="SourceGraphic"/>
|
|
18
|
+
</filter>
|
|
19
|
+
</defs>
|
|
20
|
+
<circle cx="110" cy="110" r="78" fill="#0F172A" fill-opacity="0.82" stroke="#1E293B" stroke-width="2"/>
|
|
21
|
+
<g filter="url(#glow)" stroke="url(#orbit)" stroke-width="6" stroke-linecap="round">
|
|
22
|
+
<ellipse cx="110" cy="110" rx="76" ry="28" transform="rotate(-23 110 110)"/>
|
|
23
|
+
<ellipse cx="110" cy="110" rx="76" ry="28" transform="rotate(23 110 110)"/>
|
|
24
|
+
<ellipse cx="110" cy="110" rx="28" ry="76"/>
|
|
25
|
+
</g>
|
|
26
|
+
<circle cx="110" cy="110" r="19" fill="url(#core)"/>
|
|
27
|
+
<circle cx="164" cy="70" r="7" fill="#22D3EE"/>
|
|
28
|
+
<circle cx="62" cy="148" r="6" fill="#C084FC"/>
|
|
29
|
+
</svg>
|