@gotza02/sequential-thinking 2026.2.30 → 2026.2.32
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 +1 -0
- package/SYSTEM_INSTRUCTION.md +8 -1
- package/dist/codestore.d.ts +102 -2
- package/dist/codestore.js +499 -17
- package/dist/graph.d.ts +10 -1
- package/dist/graph.js +471 -93
- package/dist/http-server.d.ts +15 -0
- package/dist/http-server.js +732 -70
- package/dist/lib.js +84 -28
- package/dist/notes.d.ts +39 -2
- package/dist/notes.js +233 -24
- package/dist/system_test.js +1 -1
- package/dist/tools/codestore.js +1 -1
- package/dist/tools/filesystem.js +359 -40
- package/dist/tools/thinking.js +7 -1
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -1,21 +1,208 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import * as crypto from 'crypto';
|
|
3
5
|
import ts from 'typescript';
|
|
6
|
+
/**
|
|
7
|
+
* ConfigResolver - Handles TypeScript paths, package.json imports, and alias resolution
|
|
8
|
+
*/
|
|
9
|
+
class ConfigResolver {
|
|
10
|
+
rootDir;
|
|
11
|
+
tsConfig = null;
|
|
12
|
+
packageJson = null;
|
|
13
|
+
baseUrl;
|
|
14
|
+
paths = new Map();
|
|
15
|
+
imports = new Map();
|
|
16
|
+
constructor(rootDir) {
|
|
17
|
+
this.rootDir = path.resolve(rootDir);
|
|
18
|
+
this.baseUrl = this.rootDir;
|
|
19
|
+
}
|
|
20
|
+
async load() {
|
|
21
|
+
await this.loadTSConfig();
|
|
22
|
+
await this.loadPackageJson();
|
|
23
|
+
}
|
|
24
|
+
async loadTSConfig() {
|
|
25
|
+
const tsConfigPaths = [
|
|
26
|
+
path.join(this.rootDir, 'tsconfig.json'),
|
|
27
|
+
path.join(this.rootDir, 'tsconfig.base.json'),
|
|
28
|
+
path.join(this.rootDir, 'tsconfig.app.json'),
|
|
29
|
+
];
|
|
30
|
+
for (const tsConfigPath of tsConfigPaths) {
|
|
31
|
+
if (existsSync(tsConfigPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(tsConfigPath, 'utf-8');
|
|
34
|
+
this.tsConfig = JSON.parse(content);
|
|
35
|
+
// Handle extends
|
|
36
|
+
if (this.tsConfig?.extends) {
|
|
37
|
+
const extendedPath = this.tsConfig.extends.startsWith('./')
|
|
38
|
+
? path.join(this.rootDir, this.tsConfig.extends)
|
|
39
|
+
: path.join(this.rootDir, 'node_modules', this.tsConfig.extends);
|
|
40
|
+
if (existsSync(extendedPath)) {
|
|
41
|
+
const extendedContent = readFileSync(extendedPath, 'utf-8');
|
|
42
|
+
const extendedConfig = JSON.parse(extendedContent);
|
|
43
|
+
this.tsConfig = {
|
|
44
|
+
...extendedConfig,
|
|
45
|
+
...this.tsConfig,
|
|
46
|
+
compilerOptions: {
|
|
47
|
+
...extendedConfig?.compilerOptions,
|
|
48
|
+
...this.tsConfig.compilerOptions
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Parse baseUrl
|
|
54
|
+
if (this.tsConfig?.compilerOptions?.baseUrl) {
|
|
55
|
+
this.baseUrl = path.resolve(this.rootDir, this.tsConfig.compilerOptions.baseUrl);
|
|
56
|
+
}
|
|
57
|
+
// Parse paths (e.g., "@/*": ["src/*"])
|
|
58
|
+
if (this.tsConfig?.compilerOptions?.paths) {
|
|
59
|
+
for (const [pattern, targets] of Object.entries(this.tsConfig.compilerOptions.paths)) {
|
|
60
|
+
const resolvedTargets = targets.map(t => path.resolve(this.baseUrl, t));
|
|
61
|
+
this.paths.set(pattern, resolvedTargets);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`Failed to load tsconfig from ${tsConfigPath}:`, error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async loadPackageJson() {
|
|
73
|
+
const packageJsonPath = path.join(this.rootDir, 'package.json');
|
|
74
|
+
if (existsSync(packageJsonPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(packageJsonPath, 'utf-8');
|
|
77
|
+
this.packageJson = JSON.parse(content);
|
|
78
|
+
// Parse ESM imports (e.g., "#internal/*": "./src/internal/*.js")
|
|
79
|
+
if (this.packageJson?.imports) {
|
|
80
|
+
for (const [key, value] of Object.entries(this.packageJson.imports)) {
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
this.imports.set(key, value);
|
|
83
|
+
}
|
|
84
|
+
else if (typeof value === 'object') {
|
|
85
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
86
|
+
this.imports.set(`${key}${subKey}`, subValue);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.warn(`Failed to load package.json:`, error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve an alias path to its actual filesystem path
|
|
99
|
+
* Examples:
|
|
100
|
+
* - "@/components/Button" -> "/project/src/components/Button"
|
|
101
|
+
* - "#/lib/utils" -> "/project/src/lib/utils"
|
|
102
|
+
*/
|
|
103
|
+
resolveAlias(importPath) {
|
|
104
|
+
// Try TypeScript paths first
|
|
105
|
+
for (const [pattern, targets] of this.paths.entries()) {
|
|
106
|
+
// Convert pattern to regex (e.g., "@/*" -> "^@/(.*)$")
|
|
107
|
+
const patternRegex = new RegExp('^' + pattern.replace(/\*/g, '(.*)') + '$');
|
|
108
|
+
const match = importPath.match(patternRegex);
|
|
109
|
+
if (match) {
|
|
110
|
+
const wildcard = match[1];
|
|
111
|
+
for (const target of targets) {
|
|
112
|
+
const resolved = target.replace(/\*/g, wildcard || '');
|
|
113
|
+
if (existsSync(resolved) || existsSync(resolved + '.ts') ||
|
|
114
|
+
existsSync(resolved + '.tsx') || existsSync(resolved + '.js')) {
|
|
115
|
+
return path.resolve(resolved);
|
|
116
|
+
}
|
|
117
|
+
// Try with index
|
|
118
|
+
const indexResolved = path.join(resolved, 'index');
|
|
119
|
+
if (existsSync(indexResolved + '.ts') || existsSync(indexResolved + '.js')) {
|
|
120
|
+
return path.resolve(indexResolved + '.ts');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Return first target even if not exists (for linking later)
|
|
124
|
+
return path.resolve(targets[0].replace(/\*/g, wildcard || ''));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Try package.json imports
|
|
128
|
+
for (const [pattern, target] of this.imports.entries()) {
|
|
129
|
+
const patternRegex = new RegExp('^' + pattern.replace(/\*/g, '(.*)') + '$');
|
|
130
|
+
const match = importPath.match(patternRegex);
|
|
131
|
+
if (match) {
|
|
132
|
+
const wildcard = match[1];
|
|
133
|
+
const resolved = target.replace(/\*/g, wildcard || '');
|
|
134
|
+
const absPath = path.resolve(this.rootDir, resolved);
|
|
135
|
+
if (existsSync(absPath))
|
|
136
|
+
return absPath;
|
|
137
|
+
return absPath;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Common aliases without config
|
|
141
|
+
const commonAliases = {
|
|
142
|
+
'@': this.rootDir,
|
|
143
|
+
'~': this.rootDir,
|
|
144
|
+
'@src': path.join(this.rootDir, 'src'),
|
|
145
|
+
'@lib': path.join(this.rootDir, 'src', 'lib'),
|
|
146
|
+
'@components': path.join(this.rootDir, 'src', 'components'),
|
|
147
|
+
'@utils': path.join(this.rootDir, 'src', 'utils'),
|
|
148
|
+
'#': path.join(this.rootDir, 'src'),
|
|
149
|
+
};
|
|
150
|
+
for (const [alias, aliasPath] of Object.entries(commonAliases)) {
|
|
151
|
+
if (importPath.startsWith(alias + '/')) {
|
|
152
|
+
const suffix = importPath.substring(alias.length + 1);
|
|
153
|
+
const resolved = path.join(aliasPath, suffix);
|
|
154
|
+
if (existsSync(resolved) || existsSync(resolved + '.ts') ||
|
|
155
|
+
existsSync(resolved + '.tsx') || existsSync(resolved + '.js')) {
|
|
156
|
+
return path.resolve(resolved);
|
|
157
|
+
}
|
|
158
|
+
return path.resolve(resolved);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get hash of config for cache invalidation
|
|
165
|
+
*/
|
|
166
|
+
getConfigHash() {
|
|
167
|
+
const configData = JSON.stringify({
|
|
168
|
+
tsConfig: this.tsConfig,
|
|
169
|
+
packageImports: this.packageJson?.imports
|
|
170
|
+
});
|
|
171
|
+
return crypto.createHash('md5').update(configData).digest('hex');
|
|
172
|
+
}
|
|
173
|
+
getBaseUrl() {
|
|
174
|
+
return this.baseUrl;
|
|
175
|
+
}
|
|
176
|
+
getPaths() {
|
|
177
|
+
return this.paths;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
4
180
|
export class ProjectKnowledgeGraph {
|
|
5
181
|
nodes = new Map();
|
|
6
182
|
rootDir = '';
|
|
7
|
-
cache = { version: '
|
|
183
|
+
cache = { version: '2.0', files: {} };
|
|
8
184
|
cachePath = '';
|
|
185
|
+
configResolver = null;
|
|
186
|
+
configHash = '';
|
|
187
|
+
// Dynamic concurrency based on file count
|
|
188
|
+
getConcurrencyLimit(fileCount) {
|
|
189
|
+
if (fileCount < 100)
|
|
190
|
+
return 20;
|
|
191
|
+
if (fileCount < 500)
|
|
192
|
+
return 50;
|
|
193
|
+
if (fileCount < 2000)
|
|
194
|
+
return 100;
|
|
195
|
+
return 150; // Cap at 150 for very large projects
|
|
196
|
+
}
|
|
9
197
|
constructor() { }
|
|
10
198
|
/**
|
|
11
199
|
* Force rebuild the graph by clearing cache first.
|
|
12
|
-
* Use this when cache seems stale or files are not detected properly.
|
|
13
200
|
*/
|
|
14
201
|
async forceRebuild(rootDir) {
|
|
15
202
|
this.rootDir = path.resolve(rootDir);
|
|
16
203
|
this.cachePath = path.join(this.rootDir, '.gemini_graph_cache.json');
|
|
17
204
|
// Clear in-memory cache
|
|
18
|
-
this.cache = { version: '
|
|
205
|
+
this.cache = { version: '2.0', files: {} };
|
|
19
206
|
// Delete cache file if exists
|
|
20
207
|
try {
|
|
21
208
|
await fs.unlink(this.cachePath);
|
|
@@ -23,7 +210,8 @@ export class ProjectKnowledgeGraph {
|
|
|
23
210
|
catch (e) {
|
|
24
211
|
// File doesn't exist, that's fine
|
|
25
212
|
}
|
|
26
|
-
//
|
|
213
|
+
// Clear config resolver cache
|
|
214
|
+
this.configResolver = null;
|
|
27
215
|
return await this.build(rootDir);
|
|
28
216
|
}
|
|
29
217
|
async build(rootDir) {
|
|
@@ -36,6 +224,12 @@ export class ProjectKnowledgeGraph {
|
|
|
36
224
|
throw new Error(`Path '${rootDir}' is not a directory.`);
|
|
37
225
|
}
|
|
38
226
|
this.nodes.clear();
|
|
227
|
+
// Initialize and load config resolver
|
|
228
|
+
if (!this.configResolver) {
|
|
229
|
+
this.configResolver = new ConfigResolver(this.rootDir);
|
|
230
|
+
await this.configResolver.load();
|
|
231
|
+
}
|
|
232
|
+
this.configHash = this.configResolver.getConfigHash();
|
|
39
233
|
await this.loadCache();
|
|
40
234
|
const files = await this.getAllFiles(this.rootDir);
|
|
41
235
|
// Step 1: Initialize nodes
|
|
@@ -54,7 +248,8 @@ export class ProjectKnowledgeGraph {
|
|
|
54
248
|
try {
|
|
55
249
|
const stats = await fs.stat(file);
|
|
56
250
|
const cached = this.cache.files[file];
|
|
57
|
-
if (cached && cached.mtime === stats.mtimeMs
|
|
251
|
+
if (cached && cached.mtime === stats.mtimeMs &&
|
|
252
|
+
this.cache.configHash === this.configHash) {
|
|
58
253
|
extractionMap.set(file, { imports: cached.imports, symbols: cached.symbols });
|
|
59
254
|
}
|
|
60
255
|
else {
|
|
@@ -65,8 +260,8 @@ export class ProjectKnowledgeGraph {
|
|
|
65
260
|
filesToParse.push(file);
|
|
66
261
|
}
|
|
67
262
|
}
|
|
68
|
-
// Step 3: Parse new/modified files concurrently
|
|
69
|
-
const CONCURRENCY_LIMIT =
|
|
263
|
+
// Step 3: Parse new/modified files concurrently with dynamic limit
|
|
264
|
+
const CONCURRENCY_LIMIT = this.getConcurrencyLimit(filesToParse.length);
|
|
70
265
|
for (let i = 0; i < filesToParse.length; i += CONCURRENCY_LIMIT) {
|
|
71
266
|
const chunk = filesToParse.slice(i, i + CONCURRENCY_LIMIT);
|
|
72
267
|
await Promise.all(chunk.map(async (file) => {
|
|
@@ -81,6 +276,8 @@ export class ProjectKnowledgeGraph {
|
|
|
81
276
|
};
|
|
82
277
|
}));
|
|
83
278
|
}
|
|
279
|
+
// Update config hash in cache
|
|
280
|
+
this.cache.configHash = this.configHash;
|
|
84
281
|
// Prune deleted files from cache
|
|
85
282
|
for (const cachedFile of Object.keys(this.cache.files)) {
|
|
86
283
|
if (!this.nodes.has(cachedFile)) {
|
|
@@ -108,18 +305,21 @@ export class ProjectKnowledgeGraph {
|
|
|
108
305
|
try {
|
|
109
306
|
const content = await fs.readFile(this.cachePath, 'utf-8');
|
|
110
307
|
const data = JSON.parse(content);
|
|
111
|
-
|
|
308
|
+
// Support both version 1.0 and 2.0
|
|
309
|
+
if (data.version === '1.0' || data.version === '2.0') {
|
|
112
310
|
this.cache = data;
|
|
113
311
|
}
|
|
114
312
|
}
|
|
115
313
|
catch (e) {
|
|
116
314
|
// Ignore cache errors (start fresh)
|
|
117
|
-
this.cache = { version: '
|
|
315
|
+
this.cache = { version: '2.0', files: {} };
|
|
118
316
|
}
|
|
119
317
|
}
|
|
120
318
|
async saveCache() {
|
|
121
319
|
try {
|
|
122
|
-
|
|
320
|
+
const tmpPath = this.cachePath + '.tmp';
|
|
321
|
+
await fs.writeFile(tmpPath, JSON.stringify(this.cache, null, 2), 'utf-8');
|
|
322
|
+
await fs.rename(tmpPath, this.cachePath);
|
|
123
323
|
}
|
|
124
324
|
catch (e) {
|
|
125
325
|
console.error('Failed to save graph cache:', e);
|
|
@@ -132,8 +332,19 @@ export class ProjectKnowledgeGraph {
|
|
|
132
332
|
for (const entry of entries) {
|
|
133
333
|
const res = path.resolve(dir, entry.name);
|
|
134
334
|
if (entry.isDirectory()) {
|
|
135
|
-
|
|
335
|
+
// Skip common directories
|
|
336
|
+
const skipDirs = [
|
|
337
|
+
'node_modules', '.git', 'dist', 'build', 'out',
|
|
338
|
+
'.gemini', 'coverage', '.npm', '.next', '.nuxt',
|
|
339
|
+
'target', 'bin', 'obj', '.venv', 'venv', '__pycache__',
|
|
340
|
+
'.cache', '.turbo', '.parcel'
|
|
341
|
+
];
|
|
342
|
+
if (skipDirs.includes(entry.name))
|
|
136
343
|
continue;
|
|
344
|
+
// Check for workspace package boundaries (optional optimization)
|
|
345
|
+
if (entry.name === 'packages' && this.isMonorepoRoot(dir)) {
|
|
346
|
+
// Include all packages in monorepo
|
|
347
|
+
}
|
|
137
348
|
try {
|
|
138
349
|
files.push(...await this.getAllFiles(res));
|
|
139
350
|
}
|
|
@@ -142,8 +353,10 @@ export class ProjectKnowledgeGraph {
|
|
|
142
353
|
}
|
|
143
354
|
}
|
|
144
355
|
else {
|
|
145
|
-
|
|
146
|
-
|
|
356
|
+
// Supported file extensions
|
|
357
|
+
const extPattern = /\.(ts|js|tsx|jsx|mjs|cjs|json|py|go|rs|java|kt|kts|scala|clj|cljc|rb|php|sh|bash|zsh|fish|c|cpp|h|hpp|cc|cxx)$/;
|
|
358
|
+
if (extPattern.test(entry.name)) {
|
|
359
|
+
// Ignore cache file
|
|
147
360
|
if (entry.name === '.gemini_graph_cache.json')
|
|
148
361
|
continue;
|
|
149
362
|
files.push(res);
|
|
@@ -157,11 +370,39 @@ export class ProjectKnowledgeGraph {
|
|
|
157
370
|
return [];
|
|
158
371
|
}
|
|
159
372
|
}
|
|
373
|
+
isMonorepoRoot(dir) {
|
|
374
|
+
// Check for pnpm workspace.yaml
|
|
375
|
+
if (existsSync(path.join(dir, 'pnpm-workspace.yaml')))
|
|
376
|
+
return true;
|
|
377
|
+
// Check for yarn workspace
|
|
378
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
379
|
+
if (existsSync(packageJsonPath)) {
|
|
380
|
+
try {
|
|
381
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
382
|
+
if (pkg.workspaces)
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
catch { }
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
160
389
|
async parseFile(filePath) {
|
|
161
390
|
const ext = path.extname(filePath);
|
|
162
|
-
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
391
|
+
if (['.ts', '.js', '.tsx', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
163
392
|
return await this.parseTypeScript(filePath);
|
|
164
393
|
}
|
|
394
|
+
else if (ext === '.py') {
|
|
395
|
+
return await this.parsePython(filePath);
|
|
396
|
+
}
|
|
397
|
+
else if (ext === '.go') {
|
|
398
|
+
return await this.parseGo(filePath);
|
|
399
|
+
}
|
|
400
|
+
else if (ext === '.rs') {
|
|
401
|
+
return await this.parseRust(filePath);
|
|
402
|
+
}
|
|
403
|
+
else if (['.java', '.kt', '.kts'].includes(ext)) {
|
|
404
|
+
return await this.parseJavaLike(filePath);
|
|
405
|
+
}
|
|
165
406
|
else {
|
|
166
407
|
return await this.parseGeneric(filePath);
|
|
167
408
|
}
|
|
@@ -184,6 +425,21 @@ export class ProjectKnowledgeGraph {
|
|
|
184
425
|
if (isExported)
|
|
185
426
|
symbols.push(`class:${node.name.text}`);
|
|
186
427
|
}
|
|
428
|
+
else if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
429
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
430
|
+
if (isExported)
|
|
431
|
+
symbols.push(`interface:${node.name.text}`);
|
|
432
|
+
}
|
|
433
|
+
else if (ts.isTypeAliasDeclaration(node) && node.name) {
|
|
434
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
435
|
+
if (isExported)
|
|
436
|
+
symbols.push(`type:${node.name.text}`);
|
|
437
|
+
}
|
|
438
|
+
else if (ts.isEnumDeclaration(node) && node.name) {
|
|
439
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
440
|
+
if (isExported)
|
|
441
|
+
symbols.push(`enum:${node.name.text}`);
|
|
442
|
+
}
|
|
187
443
|
else if (ts.isVariableStatement(node)) {
|
|
188
444
|
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
189
445
|
if (isExported) {
|
|
@@ -199,7 +455,6 @@ export class ProjectKnowledgeGraph {
|
|
|
199
455
|
imports.push(node.moduleSpecifier.text);
|
|
200
456
|
}
|
|
201
457
|
}
|
|
202
|
-
// 2. Dynamic imports: import('...')
|
|
203
458
|
else if (ts.isCallExpression(node)) {
|
|
204
459
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
|
|
205
460
|
const arg = node.arguments[0];
|
|
@@ -207,7 +462,6 @@ export class ProjectKnowledgeGraph {
|
|
|
207
462
|
imports.push(arg.text);
|
|
208
463
|
}
|
|
209
464
|
}
|
|
210
|
-
// 3. CommonJS: require('...')
|
|
211
465
|
else if (ts.isIdentifier(node.expression) && node.expression.text === 'require' && node.arguments.length > 0) {
|
|
212
466
|
const arg = node.arguments[0];
|
|
213
467
|
if (ts.isStringLiteral(arg)) {
|
|
@@ -215,7 +469,6 @@ export class ProjectKnowledgeGraph {
|
|
|
215
469
|
}
|
|
216
470
|
}
|
|
217
471
|
}
|
|
218
|
-
// 4. Import Equals: import x = require('...')
|
|
219
472
|
else if (ts.isImportEqualsDeclaration(node)) {
|
|
220
473
|
if (ts.isExternalModuleReference(node.moduleReference)) {
|
|
221
474
|
if (ts.isStringLiteral(node.moduleReference.expression)) {
|
|
@@ -233,74 +486,175 @@ export class ProjectKnowledgeGraph {
|
|
|
233
486
|
return { imports: [], symbols: [] };
|
|
234
487
|
}
|
|
235
488
|
}
|
|
236
|
-
async
|
|
489
|
+
async parsePython(filePath) {
|
|
237
490
|
try {
|
|
238
491
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
239
492
|
const imports = [];
|
|
240
493
|
const symbols = [];
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (dots === 1) {
|
|
264
|
-
imp = `./${name}`;
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
imp = `${'../'.repeat(dots - 1)}${name}`;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
494
|
+
// Python imports with better regex
|
|
495
|
+
const simpleImportMatches = content.matchAll(/^\s*import\s+([^#\n]+)/gm);
|
|
496
|
+
for (const match of simpleImportMatches) {
|
|
497
|
+
match[1].split(',').forEach(s => {
|
|
498
|
+
const clean = s.trim().split(/\s+/)[0];
|
|
499
|
+
if (clean && !clean.startsWith('.'))
|
|
500
|
+
imports.push(clean);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
// from . import x, from ..package import y
|
|
504
|
+
const fromImportMatches = content.matchAll(/^\s*from\s+([.\w]+)\s+import/gm);
|
|
505
|
+
for (const match of fromImportMatches) {
|
|
506
|
+
let imp = match[1];
|
|
507
|
+
if (imp.startsWith('.')) {
|
|
508
|
+
const matchDots = imp.match(/^(\.+)(.*)/);
|
|
509
|
+
if (matchDots) {
|
|
510
|
+
const dots = matchDots[1].length;
|
|
511
|
+
const name = matchDots[2];
|
|
512
|
+
if (dots === 1)
|
|
513
|
+
imp = `./${name}`;
|
|
514
|
+
else
|
|
515
|
+
imp = `${'../'.repeat(dots - 1)}${name}`;
|
|
270
516
|
}
|
|
271
|
-
imports.push(imp);
|
|
272
517
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
518
|
+
imports.push(imp);
|
|
519
|
+
}
|
|
520
|
+
// Python symbols (skip underscored/private)
|
|
521
|
+
const funcMatches = content.matchAll(/^def\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm);
|
|
522
|
+
for (const match of funcMatches)
|
|
523
|
+
symbols.push(`def:${match[1]}`);
|
|
524
|
+
const classMatches = content.matchAll(/^class\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm);
|
|
525
|
+
for (const match of classMatches)
|
|
526
|
+
symbols.push(`class:${match[1]}`);
|
|
527
|
+
return { imports, symbols };
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
console.error(`Error parsing Python file ${filePath}:`, error);
|
|
531
|
+
return { imports: [], symbols: [] };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async parseGo(filePath) {
|
|
535
|
+
try {
|
|
536
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
537
|
+
const imports = [];
|
|
538
|
+
const symbols = [];
|
|
539
|
+
// Single line: import "fmt"
|
|
540
|
+
const singleImportMatches = content.matchAll(/import\s+"([^"]+)"/g);
|
|
541
|
+
for (const match of singleImportMatches)
|
|
542
|
+
imports.push(match[1]);
|
|
543
|
+
// Block: import ( "fmt"; "os" )
|
|
544
|
+
const blockImportMatches = content.matchAll(/import\s+\(([\s\S]*?)\)/g);
|
|
545
|
+
for (const match of blockImportMatches) {
|
|
546
|
+
const block = match[1];
|
|
547
|
+
const innerMatches = block.matchAll(/"([^"]+)"/g);
|
|
548
|
+
for (const im of innerMatches)
|
|
549
|
+
imports.push(im[1]);
|
|
550
|
+
}
|
|
551
|
+
// Go symbols
|
|
552
|
+
const funcMatches = content.matchAll(/^func\s+(?:\([^\)]*\)\s+)?([A-Z][a-zA-Z0-9_]*)/gm);
|
|
553
|
+
for (const match of funcMatches)
|
|
554
|
+
symbols.push(`func:${match[1]}`);
|
|
555
|
+
const typeMatches = content.matchAll(/^type\s+([A-Z][a-zA-Z0-9_]*)\s+(?:struct|interface)/gm);
|
|
556
|
+
for (const match of typeMatches)
|
|
557
|
+
symbols.push(`type:${match[1]}`);
|
|
558
|
+
return { imports, symbols };
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
console.error(`Error parsing Go file ${filePath}:`, error);
|
|
562
|
+
return { imports: [], symbols: [] };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async parseRust(filePath) {
|
|
566
|
+
try {
|
|
567
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
568
|
+
const imports = [];
|
|
569
|
+
const symbols = [];
|
|
570
|
+
// use crate::module::item;
|
|
571
|
+
const useMatches = content.matchAll(/^use\s+([^;]+);/gm);
|
|
572
|
+
for (const match of useMatches) {
|
|
573
|
+
imports.push(match[1].trim());
|
|
574
|
+
}
|
|
575
|
+
// mod statement
|
|
576
|
+
const modMatches = content.matchAll(/^mod\s+([a-z][a-z0-9_]*)/gm);
|
|
577
|
+
for (const match of modMatches)
|
|
578
|
+
symbols.push(`mod:${match[1]}`);
|
|
579
|
+
// pub fn / pub struct / pub enum / pub trait
|
|
580
|
+
const pubMatches = content.matchAll(/^pub\s+(fn|struct|enum|trait)\s+([A-Za-z][A-Za-z0-9_]*)/gm);
|
|
581
|
+
for (const match of pubMatches)
|
|
582
|
+
symbols.push(`${match[1]}:${match[2]}`);
|
|
583
|
+
// impl blocks
|
|
584
|
+
const implMatches = content.matchAll(/^impl\s+([A-Z][A-Za-z0-9_]*)/gm);
|
|
585
|
+
for (const match of implMatches)
|
|
586
|
+
symbols.push(`impl:${match[1]}`);
|
|
587
|
+
return { imports, symbols };
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
return { imports: [], symbols: [] };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async parseJavaLike(filePath) {
|
|
594
|
+
try {
|
|
595
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
596
|
+
const imports = [];
|
|
597
|
+
const symbols = [];
|
|
598
|
+
// import package.Class;
|
|
599
|
+
const importMatches = content.matchAll(/^import\s+([^;]+);/gm);
|
|
600
|
+
for (const match of importMatches)
|
|
601
|
+
imports.push(match[1].trim());
|
|
602
|
+
// package declaration
|
|
603
|
+
const pkgMatch = content.match(/^package\s+([^;]+);/m);
|
|
604
|
+
if (pkgMatch)
|
|
605
|
+
symbols.push(`package:${pkgMatch[1]}`);
|
|
606
|
+
// public class/interface/enum
|
|
607
|
+
const classMatches = content.matchAll(/^public\s+(?:static\s+)?(?:final\s+)?(?:abstract\s+)?(class|interface|enum|record)\s+([A-Z][A-Za-z0-9_]*)/gm);
|
|
608
|
+
for (const match of classMatches)
|
|
609
|
+
symbols.push(`${match[1]}:${match[2]}`);
|
|
610
|
+
// public methods
|
|
611
|
+
const methodMatches = content.matchAll(/^public\s+(?:static\s+)?(?:synchronized\s+)?(?:final\s+)?(?:\w+(?:<[^>]+>)?)\s+([a-z][a-zA-Z0-9_]*)\s*\(/gm);
|
|
612
|
+
for (const match of methodMatches)
|
|
613
|
+
symbols.push(`method:${match[1]}`);
|
|
614
|
+
return { imports, symbols };
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
return { imports: [], symbols: [] };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async parseGeneric(filePath) {
|
|
621
|
+
try {
|
|
622
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
623
|
+
const imports = [];
|
|
624
|
+
const symbols = [];
|
|
625
|
+
const ext = path.extname(filePath);
|
|
626
|
+
// C/C++
|
|
627
|
+
if (['.c', '.cpp', '.h', '.hpp', '.cc', '.cxx'].includes(ext)) {
|
|
628
|
+
const includeMatches = content.matchAll(/#include\s*[<"]([^>"]+)[>"]/g);
|
|
629
|
+
for (const match of includeMatches)
|
|
630
|
+
imports.push(match[1]);
|
|
631
|
+
const funcMatches = content.matchAll(/^\w[\w\s*]+\s+(\w+)\s*\([^)]*\)\s*{/gm);
|
|
632
|
+
for (const match of funcMatches)
|
|
633
|
+
symbols.push(`function:${match[1]}`);
|
|
634
|
+
}
|
|
635
|
+
// Ruby
|
|
636
|
+
else if (ext === '.rb') {
|
|
637
|
+
const requireMatches = content.matchAll(/require\s+['"]([^'"]+)['"]/g);
|
|
638
|
+
for (const match of requireMatches)
|
|
639
|
+
imports.push(match[1]);
|
|
640
|
+
const defMatches = content.matchAll(/^def\s+([a-z_][a-z0-9_!?]*)/gm);
|
|
641
|
+
for (const match of defMatches)
|
|
276
642
|
symbols.push(`def:${match[1]}`);
|
|
277
|
-
const
|
|
278
|
-
for (const match of
|
|
643
|
+
const classMatches = content.matchAll(/^class\s+([A-Z][A-Za-z0-9_]*)/gm);
|
|
644
|
+
for (const match of classMatches)
|
|
279
645
|
symbols.push(`class:${match[1]}`);
|
|
280
646
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// Block: import ( "fmt"; "os" )
|
|
288
|
-
const blockImportMatches = content.matchAll(/import\s+\(([\s\S]*?)\)/g);
|
|
289
|
-
for (const match of blockImportMatches) {
|
|
290
|
-
const block = match[1];
|
|
291
|
-
const innerMatches = block.matchAll(/"([^\"]+)"/g);
|
|
292
|
-
for (const im of innerMatches)
|
|
293
|
-
imports.push(im[1]);
|
|
294
|
-
}
|
|
295
|
-
// 2. Go Symbols
|
|
296
|
-
// Functions: func Name(...)
|
|
297
|
-
const funcMatches = content.matchAll(/^func\s+([a-zA-Z0-9_]+)/gm);
|
|
647
|
+
// PHP
|
|
648
|
+
else if (ext === '.php') {
|
|
649
|
+
const useMatches = content.matchAll(/^use\s+([^;]+);/gm);
|
|
650
|
+
for (const match of useMatches)
|
|
651
|
+
imports.push(match[1].trim());
|
|
652
|
+
const funcMatches = content.matchAll(/^function\s+([a-z_][a-z0-9_]*)/gm);
|
|
298
653
|
for (const match of funcMatches)
|
|
299
|
-
symbols.push(`
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
symbols.push(`type:${match[1]}`);
|
|
654
|
+
symbols.push(`function:${match[1]}`);
|
|
655
|
+
const classMatches = content.matchAll(/^class\s+([A-Z][A-Za-z0-9_]*)/gm);
|
|
656
|
+
for (const match of classMatches)
|
|
657
|
+
symbols.push(`class:${match[1]}`);
|
|
304
658
|
}
|
|
305
659
|
return { imports, symbols };
|
|
306
660
|
}
|
|
@@ -316,12 +670,24 @@ export class ProjectKnowledgeGraph {
|
|
|
316
670
|
currentNode.symbols = symbols;
|
|
317
671
|
for (const importPath of rawImports) {
|
|
318
672
|
let resolvedPath = null;
|
|
319
|
-
|
|
320
|
-
|
|
673
|
+
// Step 1: Try alias resolution
|
|
674
|
+
if (this.configResolver && !importPath.startsWith('.')) {
|
|
675
|
+
resolvedPath = this.configResolver.resolveAlias(importPath);
|
|
676
|
+
if (resolvedPath && !this.nodes.has(resolvedPath)) {
|
|
677
|
+
// Try with extensions
|
|
678
|
+
resolvedPath = await this.resolvePath(path.dirname(resolvedPath), path.basename(resolvedPath));
|
|
679
|
+
}
|
|
321
680
|
}
|
|
322
|
-
|
|
323
|
-
|
|
681
|
+
// Step 2: Try relative/regular path resolution
|
|
682
|
+
if (!resolvedPath) {
|
|
683
|
+
if (importPath.startsWith('.')) {
|
|
684
|
+
resolvedPath = await this.resolvePath(path.dirname(filePath), importPath);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
resolvedPath = await this.resolvePath(this.configResolver?.getBaseUrl() || this.rootDir, importPath);
|
|
688
|
+
}
|
|
324
689
|
}
|
|
690
|
+
// Step 3: Link nodes
|
|
325
691
|
if (resolvedPath && this.nodes.has(resolvedPath)) {
|
|
326
692
|
if (!currentNode.imports.includes(resolvedPath)) {
|
|
327
693
|
currentNode.imports.push(resolvedPath);
|
|
@@ -331,8 +697,7 @@ export class ProjectKnowledgeGraph {
|
|
|
331
697
|
}
|
|
332
698
|
}
|
|
333
699
|
else {
|
|
334
|
-
//
|
|
335
|
-
// Ignore relative paths that failed to resolve (broken links)
|
|
700
|
+
// Keep external package references
|
|
336
701
|
if (!importPath.startsWith('.') && !path.isAbsolute(importPath)) {
|
|
337
702
|
if (!currentNode.imports.includes(importPath)) {
|
|
338
703
|
currentNode.imports.push(importPath);
|
|
@@ -347,15 +712,33 @@ export class ProjectKnowledgeGraph {
|
|
|
347
712
|
if (this.nodes.has(absolutePath)) {
|
|
348
713
|
return absolutePath;
|
|
349
714
|
}
|
|
350
|
-
// 2. Try appending extensions
|
|
351
|
-
const extensions = [
|
|
715
|
+
// 2. Try appending extensions (expanded list)
|
|
716
|
+
const extensions = [
|
|
717
|
+
'.ts', '.tsx', '.jsx', '.js', '.mjs', '.cjs',
|
|
718
|
+
'.json', '.py', '.go', '.rs',
|
|
719
|
+
'/index.ts', '/index.tsx', '/index.jsx', '/index.js', '/index.mjs',
|
|
720
|
+
];
|
|
352
721
|
for (const ext of extensions) {
|
|
353
722
|
const p = absolutePath + ext;
|
|
354
723
|
if (this.nodes.has(p)) {
|
|
355
724
|
return p;
|
|
356
725
|
}
|
|
357
726
|
}
|
|
358
|
-
// 3. Try
|
|
727
|
+
// 3. Try directory -> index file
|
|
728
|
+
if (!path.extname(absolutePath)) {
|
|
729
|
+
const indexPaths = [
|
|
730
|
+
absolutePath + '/index.ts',
|
|
731
|
+
absolutePath + '/index.js',
|
|
732
|
+
absolutePath + '/index.tsx',
|
|
733
|
+
absolutePath + '/index.jsx',
|
|
734
|
+
];
|
|
735
|
+
for (const p of indexPaths) {
|
|
736
|
+
if (this.nodes.has(p)) {
|
|
737
|
+
return p;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// 4. Try .js -> .ts mapping (ESM style imports)
|
|
359
742
|
if (absolutePath.endsWith('.js')) {
|
|
360
743
|
const tsPath = absolutePath.replace(/\.js$/, '.ts');
|
|
361
744
|
if (this.nodes.has(tsPath))
|
|
@@ -371,10 +754,8 @@ export class ProjectKnowledgeGraph {
|
|
|
371
754
|
}
|
|
372
755
|
getRelationships(filePath) {
|
|
373
756
|
const absolutePath = path.resolve(this.rootDir, filePath);
|
|
374
|
-
// Try to match exact or with extensions
|
|
375
757
|
let node = this.nodes.get(absolutePath);
|
|
376
758
|
if (!node) {
|
|
377
|
-
// Fallback search
|
|
378
759
|
for (const [key, value] of this.nodes.entries()) {
|
|
379
760
|
if (key.endsWith(filePath)) {
|
|
380
761
|
node = value;
|
|
@@ -390,7 +771,7 @@ export class ProjectKnowledgeGraph {
|
|
|
390
771
|
if (path.isAbsolute(p)) {
|
|
391
772
|
return path.relative(this.rootDir, p);
|
|
392
773
|
}
|
|
393
|
-
return p;
|
|
774
|
+
return p;
|
|
394
775
|
}),
|
|
395
776
|
importedBy: node.importedBy.map(p => path.relative(this.rootDir, p)),
|
|
396
777
|
symbols: node.symbols
|
|
@@ -417,7 +798,6 @@ export class ProjectKnowledgeGraph {
|
|
|
417
798
|
dependencies: [],
|
|
418
799
|
dependents: []
|
|
419
800
|
};
|
|
420
|
-
// Get symbols and paths for what this file imports
|
|
421
801
|
for (const imp of node.imports) {
|
|
422
802
|
const impNode = this.nodes.get(imp);
|
|
423
803
|
if (impNode) {
|
|
@@ -427,7 +807,6 @@ export class ProjectKnowledgeGraph {
|
|
|
427
807
|
});
|
|
428
808
|
}
|
|
429
809
|
}
|
|
430
|
-
// Get symbols and paths for what imports this file
|
|
431
810
|
for (const dep of node.importedBy) {
|
|
432
811
|
const depNode = this.nodes.get(dep);
|
|
433
812
|
if (depNode) {
|
|
@@ -443,6 +822,8 @@ export class ProjectKnowledgeGraph {
|
|
|
443
822
|
return {
|
|
444
823
|
root: this.rootDir,
|
|
445
824
|
fileCount: this.nodes.size,
|
|
825
|
+
configHash: this.configHash,
|
|
826
|
+
aliasCount: this.configResolver?.getPaths().size || 0,
|
|
446
827
|
mostReferencedFiles: [...this.nodes.values()]
|
|
447
828
|
.sort((a, b) => b.importedBy.length - a.importedBy.length)
|
|
448
829
|
.slice(0, 5)
|
|
@@ -456,16 +837,13 @@ export class ProjectKnowledgeGraph {
|
|
|
456
837
|
const lines = ['graph TD'];
|
|
457
838
|
const fileToId = new Map();
|
|
458
839
|
let idCounter = 0;
|
|
459
|
-
// Assign IDs
|
|
460
840
|
for (const [filePath, _] of this.nodes) {
|
|
461
841
|
const relative = path.relative(this.rootDir, filePath);
|
|
462
842
|
const id = `N${idCounter++}`;
|
|
463
843
|
fileToId.set(filePath, id);
|
|
464
|
-
// Escape quotes in label
|
|
465
844
|
const label = relative.replace(/"/g, "'");
|
|
466
845
|
lines.push(` ${id}["${label}"]`);
|
|
467
846
|
}
|
|
468
|
-
// Add Edges
|
|
469
847
|
for (const [filePath, node] of this.nodes) {
|
|
470
848
|
const sourceId = fileToId.get(filePath);
|
|
471
849
|
for (const importPath of node.imports) {
|
|
@@ -475,6 +853,6 @@ export class ProjectKnowledgeGraph {
|
|
|
475
853
|
}
|
|
476
854
|
}
|
|
477
855
|
}
|
|
478
|
-
return lines.join('
|
|
856
|
+
return lines.join('\n');
|
|
479
857
|
}
|
|
480
858
|
}
|