devsquad 3.6.0__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 (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ CI Feedback Adapter (P1-5)
5
+
6
+ Reads CI results and injects context into dispatch pipeline:
7
+ - Parse CI output (pytest, Jest, coverage, lint, build)
8
+ - Extract key metrics: pass/fail, coverage %, errors
9
+ - Generate structured context for Worker prompts
10
+ - Provide actionable feedback for failed checks
11
+
12
+ Spec reference: SPEC_V35_Agent_Skills_Quality_Framework.md Section 7.5
13
+ """
14
+
15
+ import json
16
+ import logging
17
+ import re
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass
26
+ class CIMetric:
27
+ """A single CI metric with value and status."""
28
+ name: str
29
+ value: Any
30
+ status: str # "pass", "fail", "warning", "skip"
31
+ details: str = ""
32
+
33
+ def to_dict(self) -> Dict[str, Any]:
34
+ return {
35
+ "name": self.name,
36
+ "value": self.value,
37
+ "status": self.status,
38
+ "details": self.details,
39
+ }
40
+
41
+
42
+ @dataclass
43
+ class CIResult:
44
+ """Complete CI run result."""
45
+ source: str # e.g., "pytest", "jest", "coverage", "lint"
46
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
47
+ metrics: List[CIMetric] = field(default_factory=list)
48
+ raw_output: str = ""
49
+ success: bool = False
50
+ duration_seconds: float = 0.0
51
+
52
+ def get_metric(self, name: str) -> Optional[CIMetric]:
53
+ for m in self.metrics:
54
+ if m.name == name:
55
+ return m
56
+ return None
57
+
58
+ def has_failures(self) -> bool:
59
+ return any(m.status == "fail" for m in self.metrics)
60
+
61
+ def to_summary(self) -> str:
62
+ lines = [f"CI Result ({self.source}): {'✅ PASS' if self.success else '❌ FAIL'}"]
63
+ for m in self.metrics:
64
+ icon = {"pass": "✅", "fail": "❌", "warning": "⚠️", "skip": "⏭️"}.get(m.status, "?")
65
+ lines.append(f" {icon} {m.name}: {m.value}")
66
+ if self.duration_seconds > 0:
67
+ lines.append(f" ⏱ Duration: {self.duration_seconds:.1f}s")
68
+ return "\n".join(lines)
69
+
70
+
71
+ @dataclass
72
+ class CIContext:
73
+ """Structured context extracted from CI results for injection into dispatch."""
74
+ overall_status: str # "all_pass", "has_failures", "no_ci_data"
75
+ summary: str = ""
76
+ actionable_items: List[Dict[str, Any]] = field(default_factory=list)
77
+ quality_gates: Dict[str, bool] = field(default_factory=dict)
78
+ recommendations: List[str] = field(default_factory=list)
79
+
80
+ def to_prompt_injection(self) -> str:
81
+ """Generate prompt-ready context string."""
82
+ lines = [
83
+ "\n## CI/CD Feedback Context\n",
84
+ f"**Overall Status**: {self.overall_status.upper()}",
85
+ "",
86
+ ]
87
+
88
+ if self.summary:
89
+ lines.append(f"{self.summary}")
90
+ lines.append("")
91
+
92
+ if self.quality_gates:
93
+ lines.append("**Quality Gates**:")
94
+ for gate_name, passed in self.quality_gates.items():
95
+ icon = "✅" if passed else "❌"
96
+ lines.append(f"- {icon} {gate_name}: {'PASS' if passed else 'FAIL'}")
97
+ lines.append("")
98
+
99
+ if self.actionable_items:
100
+ lines.append("**Action Items**:")
101
+ for item in self.actionable_items[:10]: # Limit to top 10
102
+ severity = item.get("severity", "info").upper()
103
+ lines.append(f"- [{severity}] {item.get('message', '')}")
104
+ lines.append("")
105
+
106
+ if self.recommendations:
107
+ lines.append("**Recommendations**:")
108
+ for rec in self.recommendations[:5]: # Limit to top 5
109
+ lines.append(f"- {rec}")
110
+
111
+ return "\n".join(lines)
112
+
113
+
114
+ class PytestParser:
115
+ """Parser for pytest-style test output."""
116
+
117
+ @staticmethod
118
+ def parse(output: str) -> CIResult:
119
+ result = CIResult(source="pytest", raw_output=output)
120
+
121
+ total_match = re.search(r'(\d+) passed', output)
122
+ fail_match = re.search(r'(\d+) failed', output)
123
+ error_match = re.search(r'(\d+) error', output)
124
+ skip_match = re.search(r'(\d+) skipped', output)
125
+
126
+ passed = int(total_match.group(1)) if total_match else 0
127
+ failed = int(fail_match.group(1)) if fail_match else 0
128
+ errors = int(error_match.group(1)) if error_match else 0
129
+ skipped = int(skip_match.group(1)) if skip_match else 0
130
+
131
+ result.metrics.append(CIMetric("tests_passed", passed, "pass"))
132
+ result.metrics.append(CIMetric("tests_failed", failed, "fail" if failed > 0 else "pass"))
133
+ result.metrics.append(CIMetric("errors", errors, "fail" if errors > 0 else "pass"))
134
+ result.metrics.append(CIMetric("skipped", skipped, "skip"))
135
+
136
+ duration_match = re.search(r'in ([\d.]+)s', output)
137
+ if duration_match:
138
+ result.duration_seconds = float(duration_match.group(1))
139
+
140
+ result.success = (failed == 0 and errors == 0)
141
+ return result
142
+
143
+
144
+ class CoverageParser:
145
+ """Parser for coverage report output."""
146
+
147
+ @staticmethod
148
+ def parse(output: str) -> CIResult:
149
+ result = CIResult(source="coverage", raw_output=output)
150
+
151
+ pct_match = re.search(r'TOTAL\s+[\d\s]+\s+(\d+)%', output)
152
+ if pct_match:
153
+ pct = int(pct_match.group(1))
154
+ result.metrics.append(
155
+ CIMetric(
156
+ "coverage_percentage",
157
+ f"{pct}%",
158
+ "pass" if pct >= 80 else ("warning" if pct >= 60 else "fail"),
159
+ f"Target: ≥80%, Actual: {pct}%",
160
+ )
161
+ )
162
+
163
+ line_match = re.search(r'(\d+)\s+missed', output)
164
+ if line_match:
165
+ missed = int(line_match.group(1))
166
+ result.metrics.append(
167
+ CIMetric("lines_missed", missed, "warning" if missed > 10 else "pass")
168
+ )
169
+
170
+ result.success = not result.has_failures()
171
+ return result
172
+
173
+
174
+ class LintParser:
175
+ """Parser for linter output (flake8, eslint, etc.)."""
176
+
177
+ @staticmethod
178
+ def parse(output: str) -> CIResult:
179
+ result = CIResult(source="lint", raw_output=output)
180
+
181
+ error_count = len(re.findall(r'^[EF]\d+', output, re.MULTILINE))
182
+ warning_count = len(re.findall(r'^W\d+', output, re.MULTILINE))
183
+
184
+ result.metrics.append(
185
+ CIMetric("errors", error_count, "fail" if error_count > 0 else "pass")
186
+ )
187
+ result.metrics.append(
188
+ CIMetric("warnings", warning_count, "warning" if warning_count > 0 else "pass")
189
+ )
190
+
191
+ result.success = (error_count == 0)
192
+ return result
193
+
194
+
195
+ class BuildParser:
196
+ """Parser for build system output."""
197
+
198
+ @staticmethod
199
+ def parse(output: str) -> CIResult:
200
+ result = CIResult(source="build", raw_output=output)
201
+
202
+ success_indicators = ['Build succeeded', 'BUILD SUCCESSFUL', 'Build OK']
203
+ fail_indicators = ['Build failed', 'BUILD FAILED', 'Error:', 'error:']
204
+
205
+ has_success = any(ind in output for ind in success_indicators)
206
+ has_failure = any(ind in output for ind in fail_indicators)
207
+
208
+ result.success = has_success and not has_failure
209
+ result.metrics.append(
210
+ CIMetric("build_status", "success" if result.success else "failed",
211
+ "pass" if result.success else "fail")
212
+ )
213
+
214
+ return result
215
+
216
+
217
+ class CIFeedbackAdapter:
218
+ """
219
+ Main adapter that reads CI results and generates dispatch context.
220
+
221
+ Usage:
222
+ adapter = CIFeedbackAdapter()
223
+
224
+ ci_result = adapter.parse_ci_output(pytest_output, "pytest")
225
+ context = adapter.generate_context([ci_result])
226
+ injection = context.to_prompt_injection()
227
+ """
228
+
229
+ PARSERS = {
230
+ "pytest": PytestParser,
231
+ "coverage": CoverageParser,
232
+ "lint": LintParser,
233
+ "build": BuildParser,
234
+ }
235
+
236
+ QUALITY_GATE_THRESHOLDS = {
237
+ "test_pass_rate": 100.0, # All tests must pass
238
+ "min_coverage": 80.0, # Minimum coverage percentage
239
+ "zero_lint_errors": True, # No lint errors allowed
240
+ "build_success": True, # Build must succeed
241
+ }
242
+
243
+ def __init__(self, strict_mode: bool = False):
244
+ self._strict_mode = strict_mode
245
+
246
+ def parse_ci_output(self, output: str, source_type: str) -> Optional[CIResult]:
247
+ """
248
+ Parse CI output based on source type.
249
+
250
+ Args:
251
+ output: Raw CI output text
252
+ source_type: One of "pytest", "coverage", "lint", "build"
253
+
254
+ Returns:
255
+ Parsed CIResult or None if parsing fails
256
+ """
257
+ parser_cls = self.PARSERS.get(source_type.lower())
258
+ if parser_cls is None:
259
+ logger.warning("Unknown CI source type: %s", source_type)
260
+ return None
261
+
262
+ try:
263
+ return parser_cls.parse(output)
264
+ except Exception as e:
265
+ logger.error("Failed to parse %s output: %s", source_type, e)
266
+ return None
267
+
268
+ def generate_context(self, results: List[CIResult]) -> CIContext:
269
+ """
270
+ Generate structured context from multiple CI results.
271
+
272
+ Args:
273
+ results: List of CIResult objects from different sources
274
+
275
+ Returns:
276
+ CIContext ready for prompt injection
277
+ """
278
+ if not results:
279
+ return CIContext(
280
+ overall_status="no_ci_data",
281
+ summary="No CI data available. Run tests before proceeding.",
282
+ )
283
+
284
+ all_pass = all(r.success for r in results)
285
+ any_fail = any(r.has_failures() for r in results)
286
+
287
+ context = CIContext(
288
+ overall_status="all_pass" if all_pass else "has_failures",
289
+ summary=self._build_summary(results),
290
+ )
291
+
292
+ context.quality_gates["all_tests_pass"] = all(
293
+ not r.has_failures() for r in results if r.source == "pytest"
294
+ ) or not any(r.source == "pytest" for r in results)
295
+ context.quality_gates["build_succeeds"] = all(
296
+ r.success for r in results if r.source == "build"
297
+ ) or not any(r.source == "build" for r in results)
298
+ context.quality_gates["no_critical_errors"] = not any_fail
299
+
300
+ context.actionable_items = self._extract_action_items(results)
301
+ context.recommendations = self._generate_recommendations(results)
302
+
303
+ return context
304
+
305
+ def _build_summary(self, results: List[CIResult]) -> str:
306
+ summaries = [r.to_summary() for r in results]
307
+ return "\n".join(summaries)
308
+
309
+ def _extract_action_items(self, results: List[CIResult]) -> List[Dict[str, Any]]:
310
+ items = []
311
+ for r in results:
312
+ for m in r.metrics:
313
+ if m.status == "fail":
314
+ items.append({
315
+ "severity": "critical",
316
+ "source": r.source,
317
+ "metric": m.name,
318
+ "message": f"{m.name} failed: {m.details or m.value}",
319
+ })
320
+ elif m.status == "warning":
321
+ items.append({
322
+ "severity": "warning",
323
+ "source": r.source,
324
+ "metric": m.name,
325
+ "message": f"{m.name} warning: {m.details or m.value}",
326
+ })
327
+ return sorted(items, key=lambda x: {"critical": 0, "warning": 1}.get(x["severity"], 2))
328
+
329
+ def _generate_recommendations(self, results: List[CIResult]) -> List[str]:
330
+ recs = []
331
+ for r in results:
332
+ if r.has_failures():
333
+ recs.append(f"Fix failing tests/checks in {r.source} before proceeding")
334
+ if r.source == "coverage":
335
+ cov = r.get_metric("coverage_percentage")
336
+ if cov and cov.status != "pass":
337
+ recs.append(f"Increase code coverage (current: {cov.value})")
338
+
339
+ if not recs:
340
+ recs.append("All CI checks passed. Safe to proceed.")
341
+ return recs
342
+
343
+
344
+ def create_default_adapter() -> CIFeedbackAdapter:
345
+ """Create adapter with default settings."""
346
+ return CIFeedbackAdapter()
347
+
348
+
349
+ def create_strict_adapter() -> CIFeedbackAdapter:
350
+ """Create adapter in strict mode."""
351
+ return CIFeedbackAdapter(strict_mode=True)
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import json
4
+ import ast
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Dict, List, Any, Optional
8
+ from dataclasses import dataclass, field
9
+ from collections import defaultdict
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class CodeNode:
16
+ name: str
17
+ node_type: str
18
+ file_path: str = ""
19
+ line_start: int = 0
20
+ line_end: int = 0
21
+ docstring: str = ""
22
+ children: List['CodeNode'] = field(default_factory=list)
23
+ imports: List[str] = field(default_factory=list)
24
+ calls: List[str] = field(default_factory=list)
25
+
26
+ def to_dict(self) -> Dict[str, Any]:
27
+ return {
28
+ 'name': self.name, 'type': self.node_type, 'file': self.file_path,
29
+ 'lines': f"{self.line_start}-{self.line_end}",
30
+ 'docstring': self.docstring[:100] if self.docstring else "",
31
+ 'children': [c.to_dict() for c in self.children],
32
+ 'imports': self.imports[:5], 'calls': self.calls[:5],
33
+ }
34
+
35
+
36
+ class CodeMapGenerator:
37
+ """
38
+ Code map generator for multi-language projects.
39
+
40
+ Scans source files and generates a structured map of:
41
+ - Modules, classes, functions
42
+ - Import dependencies
43
+ - Call relationships
44
+ - Documentation strings
45
+
46
+ Supports Python (AST), JavaScript/TypeScript (regex), Go (regex).
47
+ """
48
+
49
+ MAX_FILE_SIZE = 1 * 1024 * 1024
50
+
51
+ def __init__(self, project_root: str = ".", parsers: Optional[List[Any]] = None):
52
+ self.project_root = Path(project_root)
53
+ self._parsers: List[Any] = parsers
54
+ self._default_parser = _PythonCompatParser()
55
+
56
+ def register_parser(self, parser: Any) -> None:
57
+ if self._parsers is None:
58
+ self._parsers = []
59
+ self._parsers.append(parser)
60
+
61
+ def generate_map(
62
+ self,
63
+ target_dir: str = None,
64
+ output_format: str = "dict",
65
+ languages: Optional[List[str]] = None,
66
+ ) -> Any:
67
+ """
68
+ Generate a code map for the target directory.
69
+
70
+ Args:
71
+ target_dir: Directory to scan (relative to project_root)
72
+ output_format: "dict", "markdown", or "json"
73
+ languages: Optional filter by language (e.g., ["python", "javascript"])
74
+
75
+ Returns:
76
+ Code map in the specified format
77
+ """
78
+ scan_dir = self.project_root / (target_dir or "")
79
+ if not scan_dir.exists():
80
+ logger.warning("Target directory does not exist: %s", scan_dir)
81
+ return {} if output_format != "markdown" else ""
82
+
83
+ modules = {}
84
+
85
+ if self._parsers:
86
+ for parser in self._parsers:
87
+ lang = self._detect_language(parser)
88
+ if languages and lang not in languages:
89
+ continue
90
+ for pattern in parser.file_patterns():
91
+ for file_path in sorted(scan_dir.rglob(pattern)):
92
+ if any(p in str(file_path) for p in parser.exclude_patterns()):
93
+ continue
94
+ if file_path.stat().st_size > self.MAX_FILE_SIZE:
95
+ continue
96
+ try:
97
+ source = file_path.read_text(encoding='utf-8')
98
+ except UnicodeDecodeError:
99
+ continue
100
+ rel_path = str(file_path.relative_to(self.project_root))
101
+ file_map = parser.parse_file(source, str(file_path))
102
+ if file_map:
103
+ modules[rel_path] = file_map
104
+ else:
105
+ for py_file in sorted(scan_dir.rglob("*.py")):
106
+ if any(p in str(py_file) for p in ["__pycache__", "test_", "_test.py", ".venv"]):
107
+ continue
108
+ rel_path = py_file.relative_to(self.project_root)
109
+ module_map = self._default_parser.scan_file(py_file)
110
+ if module_map:
111
+ modules[str(rel_path)] = module_map
112
+
113
+ if output_format == "markdown":
114
+ return self._to_markdown(modules)
115
+ elif output_format == "json":
116
+ return json.dumps(modules, indent=2, ensure_ascii=False)
117
+ return modules
118
+
119
+ def _detect_language(self, parser: Any) -> str:
120
+ patterns = parser.file_patterns()
121
+ if not patterns:
122
+ return "unknown"
123
+ ext = patterns[0].lstrip("*")
124
+ lang_map = {".py": "python", ".js": "javascript", ".jsx": "javascript",
125
+ ".ts": "javascript", ".tsx": "javascript", ".go": "go"}
126
+ return lang_map.get(ext, ext.lstrip("."))
127
+
128
+ def _to_markdown(self, modules: Dict[str, Any]) -> str:
129
+ lines = ["# Code Map", ""]
130
+ for file_path, info in modules.items():
131
+ lang = info.get("language", "python")
132
+ lines.append(f"## {file_path} `{lang}`")
133
+ lines.append(f"- Classes: {info.get('total_classes', 0)} | Functions: {info.get('total_functions', 0)}")
134
+ if info.get('imports'):
135
+ lines.append(f"- Imports: {', '.join(info['imports'][:10])}")
136
+ for node in info.get('nodes', []):
137
+ ntype = node.get('type', 'function')
138
+ icon = {"class": "📦", "struct": "🏗️", "interface": "🔌"}.get(ntype, "⚡")
139
+ lines.append(f" - {icon} **{node['name']}** ({ntype})")
140
+ if node.get('docstring'):
141
+ lines.append(f" > {node['docstring']}")
142
+ for child in node.get('children', []):
143
+ lines.append(f" - ⚡ `{child['name']}`")
144
+ lines.append("")
145
+ return "\n".join(lines)
146
+
147
+ def get_dependency_graph(self, target_dir: str = None) -> Dict[str, List[str]]:
148
+ scan_dir = self.project_root / (target_dir or "")
149
+ graph = {}
150
+
151
+ if self._parsers:
152
+ for parser in self._parsers:
153
+ for pattern in parser.file_patterns():
154
+ for file_path in sorted(scan_dir.rglob(pattern)):
155
+ if any(p in str(file_path) for p in parser.exclude_patterns()):
156
+ continue
157
+ try:
158
+ source = file_path.read_text(encoding='utf-8')
159
+ except UnicodeDecodeError:
160
+ continue
161
+ rel_path = str(file_path.relative_to(self.project_root))
162
+ deps = parser.extract_dependencies(source)
163
+ if deps:
164
+ graph[rel_path] = deps
165
+ else:
166
+ for py_file in sorted(scan_dir.rglob("*.py")):
167
+ if "__pycache__" in str(py_file):
168
+ continue
169
+ try:
170
+ source = py_file.read_text(encoding='utf-8')
171
+ tree = ast.parse(source)
172
+ except (SyntaxError, UnicodeDecodeError):
173
+ continue
174
+ rel_path = str(py_file.relative_to(self.project_root))
175
+ deps = set()
176
+ for node in ast.walk(tree):
177
+ if isinstance(node, ast.ImportFrom) and node.module:
178
+ deps.add(node.module)
179
+ elif isinstance(node, ast.Import):
180
+ for alias in node.names:
181
+ deps.add(alias.name)
182
+ graph[rel_path] = sorted(deps)
183
+ return graph
184
+
185
+
186
+ class _PythonCompatParser:
187
+ """Internal Python parser maintaining backward compatibility with original CodeMapGenerator."""
188
+
189
+ def scan_file(self, file_path: Path) -> Optional[Dict[str, Any]]:
190
+ try:
191
+ if file_path.stat().st_size > CodeMapGenerator.MAX_FILE_SIZE:
192
+ return None
193
+ source = file_path.read_text(encoding='utf-8')
194
+ tree = ast.parse(source)
195
+ except (SyntaxError, UnicodeDecodeError):
196
+ return None
197
+
198
+ file_imports = []
199
+ top_level = []
200
+
201
+ for node in ast.iter_child_nodes(tree):
202
+ if isinstance(node, ast.Import):
203
+ for alias in node.names:
204
+ file_imports.append(alias.name)
205
+ elif isinstance(node, ast.ImportFrom):
206
+ module = node.module or ""
207
+ file_imports.append(module)
208
+
209
+ if isinstance(node, ast.ClassDef):
210
+ top_level.append(self._parse_class(node, str(file_path)))
211
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
212
+ top_level.append(self._parse_function(node, str(file_path)))
213
+
214
+ return {
215
+ 'file': str(file_path.name),
216
+ 'imports': file_imports[:20],
217
+ 'nodes': [n.to_dict() for n in top_level],
218
+ 'total_classes': sum(1 for n in top_level if n.node_type == 'class'),
219
+ 'total_functions': sum(1 for n in top_level if n.node_type == 'function'),
220
+ }
221
+
222
+ def _parse_class(self, node: ast.ClassDef, file_path: str) -> CodeNode:
223
+ docstring = ast.get_docstring(node) or ""
224
+ methods = []
225
+ for item in node.body:
226
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
227
+ methods.append(self._parse_function(item, file_path))
228
+ return CodeNode(
229
+ name=node.name, node_type="class", file_path=file_path,
230
+ line_start=node.lineno, line_end=node.end_lineno or node.lineno,
231
+ docstring=docstring, children=methods,
232
+ )
233
+
234
+ def _parse_function(self, node, file_path: str) -> CodeNode:
235
+ docstring = ast.get_docstring(node) or ""
236
+ calls = []
237
+ for child in ast.walk(node):
238
+ if isinstance(child, ast.Call):
239
+ if isinstance(child.func, ast.Name):
240
+ calls.append(child.func.id)
241
+ elif isinstance(child.func, ast.Attribute):
242
+ calls.append(child.func.attr)
243
+ return CodeNode(
244
+ name=node.name, node_type="function", file_path=file_path,
245
+ line_start=node.lineno, line_end=node.end_lineno or node.lineno,
246
+ docstring=docstring, calls=list(set(calls))[:10],
247
+ )