skill-seekers 2.7.3__py3-none-any.whl

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.
Files changed (79) hide show
  1. skill_seekers/__init__.py +22 -0
  2. skill_seekers/cli/__init__.py +39 -0
  3. skill_seekers/cli/adaptors/__init__.py +120 -0
  4. skill_seekers/cli/adaptors/base.py +221 -0
  5. skill_seekers/cli/adaptors/claude.py +485 -0
  6. skill_seekers/cli/adaptors/gemini.py +453 -0
  7. skill_seekers/cli/adaptors/markdown.py +269 -0
  8. skill_seekers/cli/adaptors/openai.py +503 -0
  9. skill_seekers/cli/ai_enhancer.py +310 -0
  10. skill_seekers/cli/api_reference_builder.py +373 -0
  11. skill_seekers/cli/architectural_pattern_detector.py +525 -0
  12. skill_seekers/cli/code_analyzer.py +1462 -0
  13. skill_seekers/cli/codebase_scraper.py +1225 -0
  14. skill_seekers/cli/config_command.py +563 -0
  15. skill_seekers/cli/config_enhancer.py +431 -0
  16. skill_seekers/cli/config_extractor.py +871 -0
  17. skill_seekers/cli/config_manager.py +452 -0
  18. skill_seekers/cli/config_validator.py +394 -0
  19. skill_seekers/cli/conflict_detector.py +528 -0
  20. skill_seekers/cli/constants.py +72 -0
  21. skill_seekers/cli/dependency_analyzer.py +757 -0
  22. skill_seekers/cli/doc_scraper.py +2332 -0
  23. skill_seekers/cli/enhance_skill.py +488 -0
  24. skill_seekers/cli/enhance_skill_local.py +1096 -0
  25. skill_seekers/cli/enhance_status.py +194 -0
  26. skill_seekers/cli/estimate_pages.py +433 -0
  27. skill_seekers/cli/generate_router.py +1209 -0
  28. skill_seekers/cli/github_fetcher.py +534 -0
  29. skill_seekers/cli/github_scraper.py +1466 -0
  30. skill_seekers/cli/guide_enhancer.py +723 -0
  31. skill_seekers/cli/how_to_guide_builder.py +1267 -0
  32. skill_seekers/cli/install_agent.py +461 -0
  33. skill_seekers/cli/install_skill.py +178 -0
  34. skill_seekers/cli/language_detector.py +614 -0
  35. skill_seekers/cli/llms_txt_detector.py +60 -0
  36. skill_seekers/cli/llms_txt_downloader.py +104 -0
  37. skill_seekers/cli/llms_txt_parser.py +150 -0
  38. skill_seekers/cli/main.py +558 -0
  39. skill_seekers/cli/markdown_cleaner.py +132 -0
  40. skill_seekers/cli/merge_sources.py +806 -0
  41. skill_seekers/cli/package_multi.py +77 -0
  42. skill_seekers/cli/package_skill.py +241 -0
  43. skill_seekers/cli/pattern_recognizer.py +1825 -0
  44. skill_seekers/cli/pdf_extractor_poc.py +1166 -0
  45. skill_seekers/cli/pdf_scraper.py +617 -0
  46. skill_seekers/cli/quality_checker.py +519 -0
  47. skill_seekers/cli/rate_limit_handler.py +438 -0
  48. skill_seekers/cli/resume_command.py +160 -0
  49. skill_seekers/cli/run_tests.py +230 -0
  50. skill_seekers/cli/setup_wizard.py +93 -0
  51. skill_seekers/cli/split_config.py +390 -0
  52. skill_seekers/cli/swift_patterns.py +560 -0
  53. skill_seekers/cli/test_example_extractor.py +1081 -0
  54. skill_seekers/cli/test_unified_simple.py +179 -0
  55. skill_seekers/cli/unified_codebase_analyzer.py +572 -0
  56. skill_seekers/cli/unified_scraper.py +932 -0
  57. skill_seekers/cli/unified_skill_builder.py +1605 -0
  58. skill_seekers/cli/upload_skill.py +162 -0
  59. skill_seekers/cli/utils.py +432 -0
  60. skill_seekers/mcp/__init__.py +33 -0
  61. skill_seekers/mcp/agent_detector.py +316 -0
  62. skill_seekers/mcp/git_repo.py +273 -0
  63. skill_seekers/mcp/server.py +231 -0
  64. skill_seekers/mcp/server_fastmcp.py +1249 -0
  65. skill_seekers/mcp/server_legacy.py +2302 -0
  66. skill_seekers/mcp/source_manager.py +285 -0
  67. skill_seekers/mcp/tools/__init__.py +115 -0
  68. skill_seekers/mcp/tools/config_tools.py +251 -0
  69. skill_seekers/mcp/tools/packaging_tools.py +826 -0
  70. skill_seekers/mcp/tools/scraping_tools.py +842 -0
  71. skill_seekers/mcp/tools/source_tools.py +828 -0
  72. skill_seekers/mcp/tools/splitting_tools.py +212 -0
  73. skill_seekers/py.typed +0 -0
  74. skill_seekers-2.7.3.dist-info/METADATA +2027 -0
  75. skill_seekers-2.7.3.dist-info/RECORD +79 -0
  76. skill_seekers-2.7.3.dist-info/WHEEL +5 -0
  77. skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
  78. skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
  79. skill_seekers-2.7.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,757 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dependency Graph Analyzer (C2.6)
4
+
5
+ Analyzes import/require/include/use statements to build dependency graphs.
6
+ Supports 9 programming languages with language-specific extraction.
7
+
8
+ Features:
9
+ - Multi-language import extraction (Python AST, others regex-based)
10
+ - Dependency graph construction with NetworkX
11
+ - Circular dependency detection
12
+ - Graph export (JSON, DOT/GraphViz, Mermaid)
13
+ - Strongly connected component analysis
14
+
15
+ Supported Languages:
16
+ - Python: import, from...import, relative imports (AST-based)
17
+ - JavaScript/TypeScript: ES6 import, CommonJS require (regex-based)
18
+ - C/C++: #include directives (regex-based)
19
+ - C#: using statements (regex, based on MS C# spec)
20
+ - Go: import statements (regex, based on Go language spec)
21
+ - Rust: use statements (regex, based on Rust reference)
22
+ - Java: import statements (regex, based on Oracle Java spec)
23
+ - Ruby: require/require_relative/load (regex, based on Ruby docs)
24
+ - PHP: require/include/use (regex, based on PHP reference)
25
+
26
+ Usage:
27
+ from dependency_analyzer import DependencyAnalyzer
28
+
29
+ analyzer = DependencyAnalyzer()
30
+ analyzer.analyze_file('src/main.py', content, 'Python')
31
+ analyzer.analyze_file('src/utils.go', go_content, 'Go')
32
+ graph = analyzer.build_graph()
33
+ cycles = analyzer.detect_cycles()
34
+
35
+ Credits:
36
+ - Regex patterns inspired by official language specifications
37
+ - NetworkX for graph algorithms: https://networkx.org/
38
+ """
39
+
40
+ import ast
41
+ import logging
42
+ import re
43
+ from dataclasses import dataclass, field
44
+ from pathlib import Path
45
+ from typing import Any
46
+
47
+ try:
48
+ import networkx as nx
49
+
50
+ NETWORKX_AVAILABLE = True
51
+ except ImportError:
52
+ NETWORKX_AVAILABLE = False
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+
57
+ @dataclass
58
+ class DependencyInfo:
59
+ """Information about a single dependency relationship."""
60
+
61
+ source_file: str
62
+ imported_module: str
63
+ import_type: str # 'import', 'from', 'require', 'include'
64
+ is_relative: bool = False
65
+ line_number: int = 0
66
+
67
+
68
+ @dataclass
69
+ class FileNode:
70
+ """Represents a file node in the dependency graph."""
71
+
72
+ file_path: str
73
+ language: str
74
+ dependencies: list[str] = field(default_factory=list)
75
+ imported_by: list[str] = field(default_factory=list)
76
+
77
+
78
+ class DependencyAnalyzer:
79
+ """
80
+ Multi-language dependency analyzer using NetworkX.
81
+
82
+ Analyzes import/require/include statements and builds dependency graphs
83
+ with circular dependency detection.
84
+ """
85
+
86
+ def __init__(self):
87
+ """Initialize dependency analyzer."""
88
+ if not NETWORKX_AVAILABLE:
89
+ raise ImportError(
90
+ "NetworkX is required for dependency analysis. Install with: pip install networkx"
91
+ )
92
+
93
+ self.graph = nx.DiGraph() # Directed graph for dependencies
94
+ self.file_dependencies: dict[str, list[DependencyInfo]] = {}
95
+ self.file_nodes: dict[str, FileNode] = {}
96
+
97
+ def analyze_file(self, file_path: str, content: str, language: str) -> list[DependencyInfo]:
98
+ """
99
+ Extract dependencies from a source file.
100
+
101
+ Args:
102
+ file_path: Path to source file
103
+ content: File content
104
+ language: Programming language (Python, JavaScript, TypeScript, C, C++, C#, Go, Rust, Java, Ruby, PHP)
105
+
106
+ Returns:
107
+ List of DependencyInfo objects
108
+ """
109
+ if language == "Python":
110
+ deps = self._extract_python_imports(content, file_path)
111
+ elif language in ("JavaScript", "TypeScript"):
112
+ deps = self._extract_js_imports(content, file_path)
113
+ elif language in ("C++", "C"):
114
+ deps = self._extract_cpp_includes(content, file_path)
115
+ elif language == "C#":
116
+ deps = self._extract_csharp_imports(content, file_path)
117
+ elif language == "Go":
118
+ deps = self._extract_go_imports(content, file_path)
119
+ elif language == "Rust":
120
+ deps = self._extract_rust_imports(content, file_path)
121
+ elif language == "Java":
122
+ deps = self._extract_java_imports(content, file_path)
123
+ elif language == "Ruby":
124
+ deps = self._extract_ruby_imports(content, file_path)
125
+ elif language == "PHP":
126
+ deps = self._extract_php_imports(content, file_path)
127
+ else:
128
+ logger.warning(f"Unsupported language: {language}")
129
+ deps = []
130
+
131
+ self.file_dependencies[file_path] = deps
132
+
133
+ # Create file node
134
+ imported_modules = [dep.imported_module for dep in deps]
135
+ self.file_nodes[file_path] = FileNode(
136
+ file_path=file_path, language=language, dependencies=imported_modules
137
+ )
138
+
139
+ return deps
140
+
141
+ def _extract_python_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
142
+ """
143
+ Extract Python import statements using AST.
144
+
145
+ Handles:
146
+ - import module
147
+ - import module as alias
148
+ - from module import name
149
+ - from . import relative
150
+ """
151
+ deps = []
152
+
153
+ try:
154
+ tree = ast.parse(content)
155
+ except SyntaxError:
156
+ logger.warning(f"Syntax error in {file_path}, skipping import extraction")
157
+ return deps
158
+
159
+ for node in ast.walk(tree):
160
+ if isinstance(node, ast.Import):
161
+ for alias in node.names:
162
+ deps.append(
163
+ DependencyInfo(
164
+ source_file=file_path,
165
+ imported_module=alias.name,
166
+ import_type="import",
167
+ is_relative=False,
168
+ line_number=node.lineno,
169
+ )
170
+ )
171
+
172
+ elif isinstance(node, ast.ImportFrom):
173
+ module = node.module or ""
174
+ is_relative = node.level > 0
175
+
176
+ # Handle relative imports
177
+ if is_relative:
178
+ module = "." * node.level + module
179
+
180
+ deps.append(
181
+ DependencyInfo(
182
+ source_file=file_path,
183
+ imported_module=module,
184
+ import_type="from",
185
+ is_relative=is_relative,
186
+ line_number=node.lineno,
187
+ )
188
+ )
189
+
190
+ return deps
191
+
192
+ def _extract_js_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
193
+ """
194
+ Extract JavaScript/TypeScript import statements.
195
+
196
+ Handles:
197
+ - import x from 'module'
198
+ - import { x } from 'module'
199
+ - import * as x from 'module'
200
+ - const x = require('module')
201
+ - require('module')
202
+ """
203
+ deps = []
204
+
205
+ # ES6 imports: import ... from 'module'
206
+ import_pattern = r"import\s+(?:[\w\s{},*]+\s+from\s+)?['\"]([^'\"]+)['\"]"
207
+ for match in re.finditer(import_pattern, content):
208
+ module = match.group(1)
209
+ line_num = content[: match.start()].count("\n") + 1
210
+ is_relative = module.startswith(".") or module.startswith("/")
211
+
212
+ deps.append(
213
+ DependencyInfo(
214
+ source_file=file_path,
215
+ imported_module=module,
216
+ import_type="import",
217
+ is_relative=is_relative,
218
+ line_number=line_num,
219
+ )
220
+ )
221
+
222
+ # CommonJS requires: require('module')
223
+ require_pattern = r"require\s*\(['\"]([^'\"]+)['\"]\)"
224
+ for match in re.finditer(require_pattern, content):
225
+ module = match.group(1)
226
+ line_num = content[: match.start()].count("\n") + 1
227
+ is_relative = module.startswith(".") or module.startswith("/")
228
+
229
+ deps.append(
230
+ DependencyInfo(
231
+ source_file=file_path,
232
+ imported_module=module,
233
+ import_type="require",
234
+ is_relative=is_relative,
235
+ line_number=line_num,
236
+ )
237
+ )
238
+
239
+ return deps
240
+
241
+ def _extract_cpp_includes(self, content: str, file_path: str) -> list[DependencyInfo]:
242
+ """
243
+ Extract C++ #include directives.
244
+
245
+ Handles:
246
+ - #include "local/header.h"
247
+ - #include <system/header.h>
248
+ """
249
+ deps = []
250
+
251
+ # Match #include statements
252
+ include_pattern = r'#include\s+[<"]([^>"]+)[>"]'
253
+ for match in re.finditer(include_pattern, content):
254
+ header = match.group(1)
255
+ line_num = content[: match.start()].count("\n") + 1
256
+
257
+ # Headers with "" are usually local, <> are system headers
258
+ is_relative = '"' in match.group(0)
259
+
260
+ deps.append(
261
+ DependencyInfo(
262
+ source_file=file_path,
263
+ imported_module=header,
264
+ import_type="include",
265
+ is_relative=is_relative,
266
+ line_number=line_num,
267
+ )
268
+ )
269
+
270
+ return deps
271
+
272
+ def _extract_csharp_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
273
+ """
274
+ Extract C# using statements.
275
+
276
+ Handles:
277
+ - using System;
278
+ - using MyNamespace;
279
+ - using static MyClass;
280
+ - using alias = Namespace;
281
+
282
+ Regex patterns based on C# language specification:
283
+ https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive
284
+ """
285
+ deps = []
286
+
287
+ # Match using statements: using [static] Namespace[.Type];
288
+ using_pattern = r"using\s+(?:static\s+)?(?:(\w+)\s*=\s*)?([A-Za-z_][\w.]*)\s*;"
289
+ for match in re.finditer(using_pattern, content):
290
+ alias = match.group(1) # Optional alias
291
+ namespace = match.group(2)
292
+ line_num = content[: match.start()].count("\n") + 1
293
+
294
+ # Skip 'using' statements for IDisposable (using var x = ...)
295
+ if "=" in match.group(0) and not alias:
296
+ continue
297
+
298
+ deps.append(
299
+ DependencyInfo(
300
+ source_file=file_path,
301
+ imported_module=namespace,
302
+ import_type="using",
303
+ is_relative=False, # C# uses absolute namespaces
304
+ line_number=line_num,
305
+ )
306
+ )
307
+
308
+ return deps
309
+
310
+ def _extract_go_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
311
+ """
312
+ Extract Go import statements.
313
+
314
+ Handles:
315
+ - import "package"
316
+ - import alias "package"
317
+ - import ( "pkg1" "pkg2" )
318
+
319
+ Regex patterns based on Go language specification:
320
+ https://go.dev/ref/spec#Import_declarations
321
+ """
322
+ deps = []
323
+
324
+ # Single import: import [alias] "package"
325
+ single_import_pattern = r'import\s+(?:(\w+)\s+)?"([^"]+)"'
326
+ for match in re.finditer(single_import_pattern, content):
327
+ match.group(1) # Optional alias
328
+ package = match.group(2)
329
+ line_num = content[: match.start()].count("\n") + 1
330
+
331
+ # Check if relative (starts with ./ or ../)
332
+ is_relative = package.startswith("./")
333
+
334
+ deps.append(
335
+ DependencyInfo(
336
+ source_file=file_path,
337
+ imported_module=package,
338
+ import_type="import",
339
+ is_relative=is_relative,
340
+ line_number=line_num,
341
+ )
342
+ )
343
+
344
+ # Multi-import block: import ( ... )
345
+ multi_import_pattern = r"import\s*\((.*?)\)"
346
+ for match in re.finditer(multi_import_pattern, content, re.DOTALL):
347
+ block = match.group(1)
348
+ block_start = match.start()
349
+
350
+ # Extract individual imports from block
351
+ import_line_pattern = r'(?:(\w+)\s+)?"([^"]+)"'
352
+ for line_match in re.finditer(import_line_pattern, block):
353
+ _alias = line_match.group(1)
354
+ package = line_match.group(2)
355
+ line_num = content[: block_start + line_match.start()].count("\n") + 1
356
+
357
+ is_relative = package.startswith("./")
358
+
359
+ deps.append(
360
+ DependencyInfo(
361
+ source_file=file_path,
362
+ imported_module=package,
363
+ import_type="import",
364
+ is_relative=is_relative,
365
+ line_number=line_num,
366
+ )
367
+ )
368
+
369
+ return deps
370
+
371
+ def _extract_rust_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
372
+ """
373
+ Extract Rust use statements.
374
+
375
+ Handles:
376
+ - use std::collections::HashMap;
377
+ - use crate::module;
378
+ - use super::sibling;
379
+ - use self::child;
380
+
381
+ Regex patterns based on Rust reference:
382
+ https://doc.rust-lang.org/reference/items/use-declarations.html
383
+ """
384
+ deps = []
385
+
386
+ # Match use statements: use path::to::item; (including curly braces with spaces)
387
+ # This pattern matches: use word::word; or use word::{item, item};
388
+ use_pattern = r"use\s+([\w:{}]+(?:\s*,\s*[\w:{}]+)*|[\w:]+::\{[^}]+\})\s*;"
389
+ for match in re.finditer(use_pattern, content):
390
+ module_path = match.group(1)
391
+ line_num = content[: match.start()].count("\n") + 1
392
+
393
+ # Determine if relative
394
+ is_relative = module_path.startswith(("self::", "super::"))
395
+
396
+ # Handle curly brace imports (use std::{io, fs})
397
+ if "{" in module_path:
398
+ # Extract base path
399
+ base_path = module_path.split("{")[0].rstrip(":")
400
+ # Extract items inside braces
401
+ items_match = re.search(r"\{([^}]+)\}", module_path)
402
+ if items_match:
403
+ items = [item.strip() for item in items_match.group(1).split(",")]
404
+ for item in items:
405
+ full_path = f"{base_path}::{item}" if base_path else item
406
+ deps.append(
407
+ DependencyInfo(
408
+ source_file=file_path,
409
+ imported_module=full_path,
410
+ import_type="use",
411
+ is_relative=is_relative,
412
+ line_number=line_num,
413
+ )
414
+ )
415
+ else:
416
+ deps.append(
417
+ DependencyInfo(
418
+ source_file=file_path,
419
+ imported_module=module_path,
420
+ import_type="use",
421
+ is_relative=is_relative,
422
+ line_number=line_num,
423
+ )
424
+ )
425
+
426
+ return deps
427
+
428
+ def _extract_java_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
429
+ """
430
+ Extract Java import statements.
431
+
432
+ Handles:
433
+ - import java.util.List;
434
+ - import java.util.*;
435
+ - import static java.lang.Math.PI;
436
+
437
+ Regex patterns based on Java language specification:
438
+ https://docs.oracle.com/javase/specs/jls/se17/html/jls-7.html#jls-7.5
439
+ """
440
+ deps = []
441
+
442
+ # Match import statements: import [static] package.Class;
443
+ import_pattern = r"import\s+(?:static\s+)?([A-Za-z_][\w.]*(?:\.\*)?)\s*;"
444
+ for match in re.finditer(import_pattern, content):
445
+ import_path = match.group(1)
446
+ line_num = content[: match.start()].count("\n") + 1
447
+
448
+ deps.append(
449
+ DependencyInfo(
450
+ source_file=file_path,
451
+ imported_module=import_path,
452
+ import_type="import",
453
+ is_relative=False, # Java uses absolute package names
454
+ line_number=line_num,
455
+ )
456
+ )
457
+
458
+ return deps
459
+
460
+ def _extract_ruby_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
461
+ """
462
+ Extract Ruby require/require_relative/load statements.
463
+
464
+ Handles:
465
+ - require 'gem_name'
466
+ - require_relative 'file'
467
+ - load 'script.rb'
468
+
469
+ Regex patterns based on Ruby documentation:
470
+ https://ruby-doc.org/core/Kernel.html#method-i-require
471
+ """
472
+ deps = []
473
+
474
+ # Match require: require 'module' or require "module"
475
+ require_pattern = r"require\s+['\"]([^'\"]+)['\"]"
476
+ for match in re.finditer(require_pattern, content):
477
+ module = match.group(1)
478
+ line_num = content[: match.start()].count("\n") + 1
479
+
480
+ deps.append(
481
+ DependencyInfo(
482
+ source_file=file_path,
483
+ imported_module=module,
484
+ import_type="require",
485
+ is_relative=False, # require looks in load path
486
+ line_number=line_num,
487
+ )
488
+ )
489
+
490
+ # Match require_relative: require_relative 'file'
491
+ require_relative_pattern = r"require_relative\s+['\"]([^'\"]+)['\"]"
492
+ for match in re.finditer(require_relative_pattern, content):
493
+ module = match.group(1)
494
+ line_num = content[: match.start()].count("\n") + 1
495
+
496
+ deps.append(
497
+ DependencyInfo(
498
+ source_file=file_path,
499
+ imported_module=module,
500
+ import_type="require_relative",
501
+ is_relative=True,
502
+ line_number=line_num,
503
+ )
504
+ )
505
+
506
+ # Match load: load 'script.rb'
507
+ load_pattern = r"load\s+['\"]([^'\"]+)['\"]"
508
+ for match in re.finditer(load_pattern, content):
509
+ module = match.group(1)
510
+ line_num = content[: match.start()].count("\n") + 1
511
+
512
+ deps.append(
513
+ DependencyInfo(
514
+ source_file=file_path,
515
+ imported_module=module,
516
+ import_type="load",
517
+ is_relative=True, # load is usually relative
518
+ line_number=line_num,
519
+ )
520
+ )
521
+
522
+ return deps
523
+
524
+ def _extract_php_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
525
+ """
526
+ Extract PHP require/include/use statements.
527
+
528
+ Handles:
529
+ - require 'file.php';
530
+ - require_once 'file.php';
531
+ - include 'file.php';
532
+ - include_once 'file.php';
533
+ - use Namespace\\Class;
534
+
535
+ Regex patterns based on PHP language reference:
536
+ https://www.php.net/manual/en/function.require.php
537
+ """
538
+ deps = []
539
+
540
+ # Match require/include: require[_once] 'file' or require[_once] "file"
541
+ require_pattern = r"(?:require|include)(?:_once)?\s+['\"]([^'\"]+)['\"]"
542
+ for match in re.finditer(require_pattern, content):
543
+ module = match.group(1)
544
+ line_num = content[: match.start()].count("\n") + 1
545
+
546
+ # Determine import type
547
+ import_type = "require" if "require" in match.group(0) else "include"
548
+
549
+ # PHP file paths are relative by default
550
+ is_relative = not module.startswith(("/", "http://", "https://"))
551
+
552
+ deps.append(
553
+ DependencyInfo(
554
+ source_file=file_path,
555
+ imported_module=module,
556
+ import_type=import_type,
557
+ is_relative=is_relative,
558
+ line_number=line_num,
559
+ )
560
+ )
561
+
562
+ # Match namespace use: use Namespace\Class;
563
+ use_pattern = r"use\s+([A-Za-z_][\w\\]*)\s*(?:as\s+\w+)?\s*;"
564
+ for match in re.finditer(use_pattern, content):
565
+ namespace = match.group(1)
566
+ line_num = content[: match.start()].count("\n") + 1
567
+
568
+ deps.append(
569
+ DependencyInfo(
570
+ source_file=file_path,
571
+ imported_module=namespace,
572
+ import_type="use",
573
+ is_relative=False, # Namespaces are absolute
574
+ line_number=line_num,
575
+ )
576
+ )
577
+
578
+ return deps
579
+
580
+ def build_graph(self) -> nx.DiGraph:
581
+ """
582
+ Build dependency graph from analyzed files.
583
+
584
+ Returns:
585
+ NetworkX DiGraph with file dependencies
586
+ """
587
+ self.graph.clear()
588
+
589
+ # Add all file nodes
590
+ for file_path, node in self.file_nodes.items():
591
+ self.graph.add_node(file_path, language=node.language)
592
+
593
+ # Add dependency edges
594
+ for file_path, deps in self.file_dependencies.items():
595
+ for dep in deps:
596
+ # Try to resolve the imported module to an actual file
597
+ target = self._resolve_import(file_path, dep.imported_module, dep.is_relative)
598
+
599
+ if target and target in self.file_nodes:
600
+ # Add edge from source to dependency
601
+ self.graph.add_edge(
602
+ file_path, target, import_type=dep.import_type, line_number=dep.line_number
603
+ )
604
+
605
+ # Update imported_by lists
606
+ if target in self.file_nodes:
607
+ self.file_nodes[target].imported_by.append(file_path)
608
+
609
+ return self.graph
610
+
611
+ def _resolve_import(
612
+ self, _source_file: str, imported_module: str, _is_relative: bool
613
+ ) -> str | None:
614
+ """
615
+ Resolve import statement to actual file path.
616
+
617
+ This is a simplified resolution - a full implementation would need
618
+ to handle module resolution rules for each language.
619
+ """
620
+ # For now, just return the imported module if it exists in our file_nodes
621
+ # In a real implementation, this would resolve relative paths, handle
622
+ # module resolution (node_modules, Python packages, etc.)
623
+
624
+ if imported_module in self.file_nodes:
625
+ return imported_module
626
+
627
+ # Try common variations
628
+ variations = [
629
+ imported_module,
630
+ f"{imported_module}.py",
631
+ f"{imported_module}.js",
632
+ f"{imported_module}.ts",
633
+ f"{imported_module}.h",
634
+ f"{imported_module}.cpp",
635
+ ]
636
+
637
+ for var in variations:
638
+ if var in self.file_nodes:
639
+ return var
640
+
641
+ return None
642
+
643
+ def detect_cycles(self) -> list[list[str]]:
644
+ """
645
+ Detect circular dependencies in the graph.
646
+
647
+ Returns:
648
+ List of cycles, where each cycle is a list of file paths
649
+ """
650
+ try:
651
+ cycles = list(nx.simple_cycles(self.graph))
652
+ if cycles:
653
+ logger.warning(f"Found {len(cycles)} circular dependencies")
654
+ for cycle in cycles:
655
+ logger.warning(f" Cycle: {' -> '.join(cycle)} -> {cycle[0]}")
656
+ return cycles
657
+ except Exception as e:
658
+ logger.error(f"Error detecting cycles: {e}")
659
+ return []
660
+
661
+ def get_strongly_connected_components(self) -> list[set[str]]:
662
+ """
663
+ Get strongly connected components (groups of mutually dependent files).
664
+
665
+ Returns:
666
+ List of sets, each containing file paths in a component
667
+ """
668
+ return list(nx.strongly_connected_components(self.graph))
669
+
670
+ def export_dot(self, output_path: str):
671
+ """
672
+ Export graph as GraphViz DOT format.
673
+
674
+ Args:
675
+ output_path: Path to save .dot file
676
+ """
677
+ try:
678
+ from networkx.drawing.nx_pydot import write_dot
679
+
680
+ write_dot(self.graph, output_path)
681
+ logger.info(f"Exported graph to DOT format: {output_path}")
682
+ except ImportError:
683
+ logger.warning("pydot not installed - cannot export to DOT format")
684
+ logger.warning("Install with: pip install pydot")
685
+
686
+ def export_json(self) -> dict[str, Any]:
687
+ """
688
+ Export graph as JSON structure.
689
+
690
+ Returns:
691
+ Dictionary with nodes and edges
692
+ """
693
+ return {
694
+ "nodes": [
695
+ {"file": node, "language": data.get("language", "Unknown")}
696
+ for node, data in self.graph.nodes(data=True)
697
+ ],
698
+ "edges": [
699
+ {
700
+ "source": source,
701
+ "target": target,
702
+ "import_type": data.get("import_type", "unknown"),
703
+ "line_number": data.get("line_number", 0),
704
+ }
705
+ for source, target, data in self.graph.edges(data=True)
706
+ ],
707
+ }
708
+
709
+ def export_mermaid(self) -> str:
710
+ """
711
+ Export graph as Mermaid diagram format.
712
+
713
+ Returns:
714
+ Mermaid diagram as string
715
+ """
716
+ lines = ["graph TD"]
717
+
718
+ # Create node labels (shorten file paths for readability)
719
+ node_ids = {}
720
+ for i, node in enumerate(self.graph.nodes()):
721
+ node_id = f"N{i}"
722
+ node_ids[node] = node_id
723
+ label = Path(node).name # Just filename
724
+ lines.append(f" {node_id}[{label}]")
725
+
726
+ # Add edges
727
+ for source, target in self.graph.edges():
728
+ source_id = node_ids[source]
729
+ target_id = node_ids[target]
730
+ lines.append(f" {source_id} --> {target_id}")
731
+
732
+ return "\n".join(lines)
733
+
734
+ def get_statistics(self) -> dict[str, Any]:
735
+ """
736
+ Get graph statistics.
737
+
738
+ Returns:
739
+ Dictionary with various statistics
740
+ """
741
+ return {
742
+ "total_files": self.graph.number_of_nodes(),
743
+ "total_dependencies": self.graph.number_of_edges(),
744
+ "circular_dependencies": len(self.detect_cycles()),
745
+ "strongly_connected_components": len(self.get_strongly_connected_components()),
746
+ "avg_dependencies_per_file": (
747
+ self.graph.number_of_edges() / self.graph.number_of_nodes()
748
+ if self.graph.number_of_nodes() > 0
749
+ else 0
750
+ ),
751
+ "files_with_no_dependencies": len(
752
+ [node for node in self.graph.nodes() if self.graph.out_degree(node) == 0]
753
+ ),
754
+ "files_not_imported": len(
755
+ [node for node in self.graph.nodes() if self.graph.in_degree(node) == 0]
756
+ ),
757
+ }