@kentwynn/kgraph 0.2.22 → 0.2.24
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 +35 -11
- package/dist/cli/commands/knowledge.js +15 -1
- 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/cognition/cognition-quality.js +14 -8
- package/dist/cognition/compact.js +2 -29
- package/dist/cognition/conclusion.js +1 -1
- package/dist/cognition/domain-records.d.ts +4 -0
- package/dist/cognition/domain-records.js +32 -0
- package/dist/config/config.js +45 -0
- package/dist/knowledge/atom-store.js +1 -1
- 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/git-utils.d.ts +5 -0
- package/dist/scanner/git-utils.js +33 -1
- 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 +65 -4
- 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/package.json +12 -1
|
@@ -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
|
-
import { getChangedFilesSince, isGitRepo } from './git-utils.js';
|
|
10
|
+
import { getChangedFilesSince, getGitIgnoredFiles, 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,19 +56,26 @@ 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);
|
|
41
66
|
const allExcludes = [...config.exclude, ...gitignorePatterns];
|
|
42
67
|
const mergedConfig = { ...config, exclude: allExcludes };
|
|
43
|
-
const
|
|
68
|
+
const rawEntries = await fg(config.include, {
|
|
44
69
|
cwd: rootPath,
|
|
45
70
|
dot: true,
|
|
46
71
|
onlyFiles: true,
|
|
47
72
|
unique: true,
|
|
48
73
|
ignore: buildFastGlobIgnore(allExcludes),
|
|
49
74
|
});
|
|
75
|
+
const gitIgnored = (await isGitRepo(rootPath))
|
|
76
|
+
? await getGitIgnoredFiles(rootPath, rawEntries)
|
|
77
|
+
: new Set();
|
|
78
|
+
const entries = rawEntries.filter((entry) => !gitIgnored.has(entry));
|
|
50
79
|
// Build lookup maps from previous scan for incremental skip
|
|
51
80
|
const prevFileByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
|
|
52
81
|
const prevSymbolsByFile = new Map();
|
|
@@ -190,7 +219,7 @@ export async function scanRepository(rootPath, config, previous) {
|
|
|
190
219
|
}
|
|
191
220
|
resolveLocalDependencies(dependencies, files);
|
|
192
221
|
relationships.push(...buildImportRelationships(dependencies));
|
|
193
|
-
relationships.push(...detectMovedFiles(previous?.files ?? [], files));
|
|
222
|
+
relationships.push(...detectMovedFiles((previous?.files ?? []).filter((file) => !shouldExclude(file.path, mergedConfig)), files));
|
|
194
223
|
return {
|
|
195
224
|
files,
|
|
196
225
|
symbols,
|
|
@@ -223,6 +252,15 @@ const SOURCE_EXTENSIONS = [
|
|
|
223
252
|
'.hpp',
|
|
224
253
|
'.hxx',
|
|
225
254
|
'.cs',
|
|
255
|
+
'.php',
|
|
256
|
+
'.swift',
|
|
257
|
+
'.rb',
|
|
258
|
+
'.rake',
|
|
259
|
+
'.lua',
|
|
260
|
+
'.dart',
|
|
261
|
+
'.ex',
|
|
262
|
+
'.exs',
|
|
263
|
+
'.scala',
|
|
226
264
|
];
|
|
227
265
|
function resolveLocalDependencies(dependencies, files) {
|
|
228
266
|
const filePaths = new Set(files.map((file) => file.path));
|
|
@@ -237,6 +275,9 @@ function resolveLocalDependencyPath(fromFile, specifier, filePaths) {
|
|
|
237
275
|
if (!specifier.startsWith('.')) {
|
|
238
276
|
return undefined;
|
|
239
277
|
}
|
|
278
|
+
if (path.posix.extname(fromFile) === '.py') {
|
|
279
|
+
return resolvePythonRelativeImportPath(fromFile, specifier, filePaths);
|
|
280
|
+
}
|
|
240
281
|
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), specifier));
|
|
241
282
|
const candidates = path.posix.extname(base)
|
|
242
283
|
? [base]
|
|
@@ -246,6 +287,26 @@ function resolveLocalDependencyPath(fromFile, specifier, filePaths) {
|
|
|
246
287
|
];
|
|
247
288
|
return candidates.find((candidate) => filePaths.has(candidate));
|
|
248
289
|
}
|
|
290
|
+
function resolvePythonRelativeImportPath(fromFile, specifier, filePaths) {
|
|
291
|
+
const match = specifier.match(/^(\.+)(.*)$/);
|
|
292
|
+
if (!match) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const [, dots, moduleName] = match;
|
|
296
|
+
const packageParts = path.posix.dirname(fromFile).split('/');
|
|
297
|
+
const levelsUp = Math.max(0, dots.length - 1);
|
|
298
|
+
const baseParts = levelsUp > 0 ? packageParts.slice(0, -levelsUp) : packageParts;
|
|
299
|
+
const modulePath = moduleName
|
|
300
|
+
.split('.')
|
|
301
|
+
.filter(Boolean)
|
|
302
|
+
.join('/');
|
|
303
|
+
const base = path.posix.normalize(path.posix.join(...baseParts, modulePath));
|
|
304
|
+
const candidates = [
|
|
305
|
+
`${base}.py`,
|
|
306
|
+
path.posix.join(base, '__init__.py'),
|
|
307
|
+
];
|
|
308
|
+
return candidates.find((candidate) => filePaths.has(candidate));
|
|
309
|
+
}
|
|
249
310
|
function buildImportRelationships(dependencies) {
|
|
250
311
|
return dependencies.map((dependency) => ({
|
|
251
312
|
sourceType: 'file',
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kentwynn/kgraph",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "Persistent repo intelligence for AI coding assistants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,16 +45,27 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@clack/prompts": "^1.3.0",
|
|
47
47
|
"@tree-sitter-grammars/tree-sitter-kotlin": "^1.1.0",
|
|
48
|
+
"@tree-sitter-grammars/tree-sitter-yaml": "^0.7.1",
|
|
48
49
|
"chalk": "^5.6.2",
|
|
49
50
|
"commander": "^12.1.0",
|
|
50
51
|
"fast-glob": "^3.3.2",
|
|
52
|
+
"tree-sitter-bash": "^0.25.1",
|
|
51
53
|
"tree-sitter-c": "^0.24.1",
|
|
52
54
|
"tree-sitter-c-sharp": "^0.23.5",
|
|
53
55
|
"tree-sitter-cpp": "^0.23.4",
|
|
56
|
+
"tree-sitter-css": "^0.25.0",
|
|
57
|
+
"tree-sitter-dart": "^1.0.0",
|
|
58
|
+
"tree-sitter-elixir": "^0.3.5",
|
|
54
59
|
"tree-sitter-go": "^0.25.0",
|
|
60
|
+
"tree-sitter-html": "^0.23.2",
|
|
55
61
|
"tree-sitter-java": "^0.23.5",
|
|
62
|
+
"tree-sitter-json": "^0.24.8",
|
|
63
|
+
"tree-sitter-lua": "^2.1.3",
|
|
64
|
+
"tree-sitter-php": "^0.24.2",
|
|
56
65
|
"tree-sitter-python": "^0.25.0",
|
|
66
|
+
"tree-sitter-ruby": "^0.23.1",
|
|
57
67
|
"tree-sitter-rust": "^0.24.0",
|
|
68
|
+
"tree-sitter-scala": "^0.24.0",
|
|
58
69
|
"typescript": "^5.9.3",
|
|
59
70
|
"web-tree-sitter": "^0.26.8",
|
|
60
71
|
"yaml": "^2.5.1"
|