@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/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: '1.0', files: {} };
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: '1.0', files: {} };
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
- // Now build fresh
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 = 20;
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
- if (data.version === '1.0') {
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: '1.0', files: {} };
315
+ this.cache = { version: '2.0', files: {} };
118
316
  }
119
317
  }
120
318
  async saveCache() {
121
319
  try {
122
- await fs.writeFile(this.cachePath, JSON.stringify(this.cache, null, 2), 'utf-8');
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
- if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist' || entry.name === '.gemini' || entry.name === 'coverage' || entry.name === '.npm')
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
- if (/\.(ts|js|tsx|jsx|json|py|go|rs|java|c|cpp|h)$/.test(entry.name)) {
146
- // Ignore cache file itself
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 parseGeneric(filePath) {
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
- const ext = path.extname(filePath);
242
- if (ext === '.py') {
243
- // 1. Python Imports
244
- // Handle: import os, sys
245
- const simpleImportMatches = content.matchAll(/^\s*import\s+([^#\n]+)/gm);
246
- for (const match of simpleImportMatches) {
247
- match[1].split(',').forEach(s => {
248
- const clean = s.trim().split(/\s+/)[0]; // Handle 'import x as y'
249
- if (clean)
250
- imports.push(clean);
251
- });
252
- }
253
- // Handle: from .module import func OR from package.module import func
254
- const fromImportMatches = content.matchAll(/^\s*from\s+([.a-zA-Z0-9_]+)\s+import/gm);
255
- for (const match of fromImportMatches) {
256
- let imp = match[1];
257
- // Convert Python relative import to path (e.g. .module -> ./module)
258
- if (imp.startsWith('.')) {
259
- const matchDots = imp.match(/^(\.+)(.*)/);
260
- if (matchDots) {
261
- const dots = matchDots[1].length;
262
- const name = matchDots[2];
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
- // 2. Python Symbols (Only top-level defs/classes to avoid nested methods)
274
- const topLevelFuncMatches = content.matchAll(/^def\s+([a-zA-Z0-9_]+)/gm);
275
- for (const match of topLevelFuncMatches)
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 topLevelClassMatches = content.matchAll(/^class\s+([a-zA-Z0-9_]+)/gm);
278
- for (const match of topLevelClassMatches)
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
- else if (ext === '.go') {
282
- // 1. Go Imports
283
- // Single line: import "fmt"
284
- const singleImportMatches = content.matchAll(/import\s+\"([^\"]+)\"/g);
285
- for (const match of singleImportMatches)
286
- imports.push(match[1]);
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(`func:${match[1]}`);
300
- // Types: type Name struct/interface
301
- const typeMatches = content.matchAll(/^type\s+([a-zA-Z0-9_]+)\s+(?:struct|interface)/gm);
302
- for (const match of typeMatches)
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
- if (importPath.startsWith('.')) {
320
- resolvedPath = await this.resolvePath(path.dirname(filePath), importPath);
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
- else {
323
- resolvedPath = await this.resolvePath(this.rootDir, importPath);
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
- // If we can't resolve to a local file, keep the original import string ONLY if it looks like an external package
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 = ['.ts', '.js', '.tsx', '.jsx', '.json', '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '/index.ts', '/index.js'];
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 handling .js -> .ts mapping (ESM style imports)
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; // Return as is for external libraries
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('\\n');
856
+ return lines.join('\n');
479
857
  }
480
858
  }