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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- 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
|
+
)
|