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,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
IntentWorkflowMapper - Maps user intent to workflow chains
|
|
5
|
+
|
|
6
|
+
Extends RoleMatcher with semantic understanding of WHAT user wants to do,
|
|
7
|
+
not just WHICH keywords they used.
|
|
8
|
+
|
|
9
|
+
When user says "fix the login bug", system should know this requires
|
|
10
|
+
debugging workflow, not generic coding.
|
|
11
|
+
|
|
12
|
+
Integration point: Called by MultiAgentDispatcher.dispatch() before role matching.
|
|
13
|
+
The detected IntentMatch is stored and passed to Coordinator/Workers.
|
|
14
|
+
|
|
15
|
+
Spec reference: SPEC_V35_Agent_Skills_Quality_Framework.md Section 6.3
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class WorkflowChainDef:
|
|
27
|
+
"""Definition of a workflow chain for a specific intent type."""
|
|
28
|
+
trigger_keywords: Dict[str, List[str]]
|
|
29
|
+
workflow_chain: List[str]
|
|
30
|
+
required_roles: List[str]
|
|
31
|
+
optional_roles: List[str] = field(default_factory=list)
|
|
32
|
+
gate: Optional[str] = None
|
|
33
|
+
gate_description: str = ""
|
|
34
|
+
anti_skip_message: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class IntentMatch:
|
|
39
|
+
"""Result of intent detection against task description."""
|
|
40
|
+
intent_type: str
|
|
41
|
+
confidence: float
|
|
42
|
+
workflow_chain: List[str] = field(default_factory=list)
|
|
43
|
+
required_roles: List[str] = field(default_factory=list)
|
|
44
|
+
optional_roles: List[str] = field(default_factory=list)
|
|
45
|
+
gate: Optional[str] = None
|
|
46
|
+
gate_description: str = ""
|
|
47
|
+
anti_skip_message: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class IntentWorkflowMapper:
|
|
51
|
+
"""
|
|
52
|
+
Maps user intent to workflow chains and required roles.
|
|
53
|
+
|
|
54
|
+
Extends RoleMatcher with semantic understanding of user INTENT.
|
|
55
|
+
Supports 3 languages (zh/en/ja) for keyword detection.
|
|
56
|
+
|
|
57
|
+
Inspired by AGENTS.md intent mapping from Agent Skills (addyosmani/agent-skills).
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
WORKFLOW_CHAINS: Dict[str, WorkflowChainDef] = {
|
|
61
|
+
"bug_fix": WorkflowChainDef(
|
|
62
|
+
trigger_keywords={
|
|
63
|
+
"zh": [
|
|
64
|
+
"修复", "修", "bug", "错误", "报错", "失败",
|
|
65
|
+
"异常", "崩溃", "缺陷", "问题",
|
|
66
|
+
],
|
|
67
|
+
"en": [
|
|
68
|
+
"fix", "bug", "error", "fail", "crash", "broken",
|
|
69
|
+
"issue", "defect", "problem", "exception",
|
|
70
|
+
],
|
|
71
|
+
"ja": [
|
|
72
|
+
"修正", "バグ", "エラー", "失敗", "異常", "クラッシュ",
|
|
73
|
+
"欠陥", "問題",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
workflow_chain=[
|
|
77
|
+
"debugging_and_error_recovery",
|
|
78
|
+
"test_driven_development",
|
|
79
|
+
],
|
|
80
|
+
required_roles=["solo-coder", "tester"],
|
|
81
|
+
optional_roles=["security"],
|
|
82
|
+
gate="prove_it_pattern",
|
|
83
|
+
gate_description=(
|
|
84
|
+
"Must have a failing reproduction test before implementing fix"
|
|
85
|
+
),
|
|
86
|
+
anti_skip_message=(
|
|
87
|
+
"Do NOT implement the fix first. "
|
|
88
|
+
"Write the test that demonstrates the bug."
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
"new_feature": WorkflowChainDef(
|
|
92
|
+
trigger_keywords={
|
|
93
|
+
"zh": [
|
|
94
|
+
"实现", "开发", "新增", "添加", "创建", "构建",
|
|
95
|
+
"功能", "特性", "做一个", "写一个",
|
|
96
|
+
],
|
|
97
|
+
"en": [
|
|
98
|
+
"implement", "develop", "add", "create", "build",
|
|
99
|
+
"feature", "new", "make a", "write a",
|
|
100
|
+
],
|
|
101
|
+
"ja": [
|
|
102
|
+
"実装", "開発", "追加", "作成", "構築", "機能",
|
|
103
|
+
"新規", "作る",
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
workflow_chain=[
|
|
107
|
+
"spec_driven_development",
|
|
108
|
+
"planning_and_task_breakdown",
|
|
109
|
+
"incremental_implementation",
|
|
110
|
+
"test_driven_development",
|
|
111
|
+
],
|
|
112
|
+
required_roles=["architect", "solo-coder", "tester"],
|
|
113
|
+
optional_roles=["product-manager", "ui-designer"],
|
|
114
|
+
gate="spec_first",
|
|
115
|
+
gate_description=(
|
|
116
|
+
"Must produce or validate a spec before writing implementation code"
|
|
117
|
+
),
|
|
118
|
+
anti_skip_message=(
|
|
119
|
+
"Do NOT start coding until the spec is reviewed and approved."
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
"security_review": WorkflowChainDef(
|
|
123
|
+
trigger_keywords={
|
|
124
|
+
"zh": [
|
|
125
|
+
"安全", "漏洞", "渗透", "审计", "加固", "注入",
|
|
126
|
+
"XSS", "SQL注入", "越权", "CSRF",
|
|
127
|
+
],
|
|
128
|
+
"en": [
|
|
129
|
+
"security", "vulnerability", "penetration", "audit",
|
|
130
|
+
"harden", "injection", "XSS", "SQL injection",
|
|
131
|
+
"OWASP", "CSRF", "auth bypass",
|
|
132
|
+
],
|
|
133
|
+
"ja": [
|
|
134
|
+
"セキュリティ", "脆弱性", "侵入", "監査", "強化",
|
|
135
|
+
"インジェクション", "OWASP", "認証バイパス",
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
workflow_chain=[
|
|
139
|
+
"security_and_hardening",
|
|
140
|
+
"code_review_and_quality",
|
|
141
|
+
],
|
|
142
|
+
required_roles=["security", "architect"],
|
|
143
|
+
optional_roles=["solo-coder"],
|
|
144
|
+
gate="owasp_checklist",
|
|
145
|
+
gate_description=(
|
|
146
|
+
"Must complete OWASP Top 10 checklist before approving"
|
|
147
|
+
),
|
|
148
|
+
anti_skip_message=(
|
|
149
|
+
"Do NOT mark as secure without systematic vulnerability assessment."
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
"code_review": WorkflowChainDef(
|
|
153
|
+
trigger_keywords={
|
|
154
|
+
"zh": ["审查", "review", "代码质量", "重构", "优化", "简化", "走查"],
|
|
155
|
+
"en": [
|
|
156
|
+
"review", "code quality", "refactor", "optimize",
|
|
157
|
+
"simplify", "walkthrough", "inspect", "quality",
|
|
158
|
+
],
|
|
159
|
+
"ja": [
|
|
160
|
+
"レビュー", "コード品質", "リファクタ", "最適化",
|
|
161
|
+
"単純化", "ウォークスルー",
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
workflow_chain=[
|
|
165
|
+
"code_review_and_quality",
|
|
166
|
+
"code_simplification",
|
|
167
|
+
],
|
|
168
|
+
required_roles=["solo-coder", "security", "tester"],
|
|
169
|
+
optional_roles=["architect"],
|
|
170
|
+
gate="change_size_limit",
|
|
171
|
+
gate_description="Changes must be ~100 lines or split into smaller reviews",
|
|
172
|
+
anti_skip_message="Do NOT approve large changesets without splitting.",
|
|
173
|
+
),
|
|
174
|
+
"performance_optimization": WorkflowChainDef(
|
|
175
|
+
trigger_keywords={
|
|
176
|
+
"zh": [
|
|
177
|
+
"性能", "优化", "慢", "加速", "延迟", "吞吐",
|
|
178
|
+
"瓶颈", "卡顿", "超时",
|
|
179
|
+
],
|
|
180
|
+
"en": [
|
|
181
|
+
"performance", "optimize", "slow", "speedup", "latency",
|
|
182
|
+
"throughput", "bottleneck", "timeout",
|
|
183
|
+
],
|
|
184
|
+
"ja": [
|
|
185
|
+
"パフォーマンス", "最適化", "遅い", "高速化",
|
|
186
|
+
"レイテンシ", "スループット", "ボトルネック",
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
workflow_chain=[
|
|
190
|
+
"performance_optimization",
|
|
191
|
+
"code_review_and_quality",
|
|
192
|
+
],
|
|
193
|
+
required_roles=["architect", "devops"],
|
|
194
|
+
optional_roles=["solo-coder"],
|
|
195
|
+
gate="measure_first",
|
|
196
|
+
gate_description="Must have baseline measurements before optimizing",
|
|
197
|
+
anti_skip_message=(
|
|
198
|
+
"Do NOT optimize without measurements. "
|
|
199
|
+
"You're likely optimizing the wrong thing."
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
"deployment": WorkflowChainDef(
|
|
203
|
+
trigger_keywords={
|
|
204
|
+
"zh": ["部署", "发布", "上线", "部署", "CI", "CD", "发布", "部署到"],
|
|
205
|
+
"en": [
|
|
206
|
+
"deploy", "release", "ship", "launch", "CI", "CD",
|
|
207
|
+
"publish", "rollout",
|
|
208
|
+
],
|
|
209
|
+
"ja": ["デプロイ", "リリース", "公開", "CI", "CD"],
|
|
210
|
+
},
|
|
211
|
+
workflow_chain=[
|
|
212
|
+
"ci_cd_and_automation",
|
|
213
|
+
"shipping_and_launch",
|
|
214
|
+
],
|
|
215
|
+
required_roles=["devops", "security"],
|
|
216
|
+
optional_roles=["architect"],
|
|
217
|
+
gate="pre_launch_checklist",
|
|
218
|
+
gate_description="Complete all 6 categories of pre-launch checks",
|
|
219
|
+
anti_skip_message=(
|
|
220
|
+
"Do NOT deploy without rollback plan and monitoring setup."
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def __init__(self, confidence_threshold: float = 0.3):
|
|
226
|
+
self._confidence_threshold = confidence_threshold
|
|
227
|
+
self._cache: Dict[str, Optional[IntentMatch]] = {}
|
|
228
|
+
self._MAX_CACHE_SIZE = 128
|
|
229
|
+
|
|
230
|
+
def detect_intent(
|
|
231
|
+
self,
|
|
232
|
+
task_description: str,
|
|
233
|
+
lang: str = "zh"
|
|
234
|
+
) -> Optional[IntentMatch]:
|
|
235
|
+
"""
|
|
236
|
+
Detect user intent from task description.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
task_description: The user's task text
|
|
240
|
+
lang: Language code ("zh", "en", or "ja")
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
IntentMatch if confidence >= threshold, else None
|
|
244
|
+
"""
|
|
245
|
+
cache_key = f"{lang}:{task_description[:100]}"
|
|
246
|
+
if cache_key in self._cache:
|
|
247
|
+
return self._cache[cache_key]
|
|
248
|
+
|
|
249
|
+
task_lower = task_description.lower()
|
|
250
|
+
|
|
251
|
+
best_match = None
|
|
252
|
+
best_score = 0.0
|
|
253
|
+
|
|
254
|
+
for intent_type, chain_def in self.WORKFLOW_CHAINS.items():
|
|
255
|
+
score = self._calculate_score(task_lower, chain_def, lang)
|
|
256
|
+
|
|
257
|
+
if score > 0 and score > best_score:
|
|
258
|
+
best_score = score
|
|
259
|
+
best_match = IntentMatch(
|
|
260
|
+
intent_type=intent_type,
|
|
261
|
+
confidence=score,
|
|
262
|
+
workflow_chain=list(chain_def.workflow_chain),
|
|
263
|
+
required_roles=list(chain_def.required_roles),
|
|
264
|
+
optional_roles=list(chain_def.optional_roles),
|
|
265
|
+
gate=chain_def.gate,
|
|
266
|
+
gate_description=chain_def.gate_description,
|
|
267
|
+
anti_skip_message=chain_def.anti_skip_message,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if best_match and best_match.confidence >= self._confidence_threshold:
|
|
271
|
+
if len(self._cache) >= self._MAX_CACHE_SIZE:
|
|
272
|
+
oldest_key = next(iter(self._cache))
|
|
273
|
+
del self._cache[oldest_key]
|
|
274
|
+
self._cache[cache_key] = best_match
|
|
275
|
+
logger.info(
|
|
276
|
+
"Intent detected: type=%s confidence=%.2f roles=%s",
|
|
277
|
+
best_match.intent_type,
|
|
278
|
+
best_match.confidence,
|
|
279
|
+
best_match.required_roles,
|
|
280
|
+
)
|
|
281
|
+
return best_match
|
|
282
|
+
|
|
283
|
+
if len(self._cache) >= self._MAX_CACHE_SIZE:
|
|
284
|
+
oldest_key = next(iter(self._cache))
|
|
285
|
+
del self._cache[oldest_key]
|
|
286
|
+
self._cache[cache_key] = None
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def _calculate_score(
|
|
290
|
+
self,
|
|
291
|
+
task_lower: str,
|
|
292
|
+
chain_def: WorkflowChainDef,
|
|
293
|
+
lang: str,
|
|
294
|
+
) -> float:
|
|
295
|
+
primary_keywords = list(chain_def.trigger_keywords.get(lang, []))
|
|
296
|
+
secondary_keywords = list(chain_def.trigger_keywords.get("en", [])) if lang != "en" else []
|
|
297
|
+
|
|
298
|
+
if not primary_keywords and not secondary_keywords:
|
|
299
|
+
return 0.0
|
|
300
|
+
|
|
301
|
+
primary_matches = sum(1 for kw in primary_keywords if kw.lower() in task_lower)
|
|
302
|
+
secondary_matches = sum(1 for kw in secondary_keywords if kw.lower() in task_lower)
|
|
303
|
+
|
|
304
|
+
if primary_matches == 0 and secondary_matches == 0:
|
|
305
|
+
return 0.0
|
|
306
|
+
|
|
307
|
+
score = 0.0
|
|
308
|
+
if primary_matches > 0:
|
|
309
|
+
score = min(0.4 + (primary_matches - 1) * 0.2, 1.0)
|
|
310
|
+
elif secondary_matches > 0:
|
|
311
|
+
score = min(0.25 + (secondary_matches - 1) * 0.15, 0.9)
|
|
312
|
+
|
|
313
|
+
return score
|
|
314
|
+
|
|
315
|
+
def get_available_intents(self) -> List[str]:
|
|
316
|
+
"""Return all available intent types."""
|
|
317
|
+
return sorted(self.WORKFLOW_CHAINS.keys())
|
|
318
|
+
|
|
319
|
+
def get_intent_details(self, intent_type: str) -> Optional[Dict[str, Any]]:
|
|
320
|
+
"""Get full details for an intent type."""
|
|
321
|
+
chain_def = self.WORKFLOW_CHAINS.get(intent_type)
|
|
322
|
+
if not chain_def:
|
|
323
|
+
return None
|
|
324
|
+
return {
|
|
325
|
+
"intent_type": intent_type,
|
|
326
|
+
"trigger_keywords": chain_def.trigger_keywords,
|
|
327
|
+
"workflow_chain": chain_def.workflow_chain,
|
|
328
|
+
"required_roles": chain_def.required_roles,
|
|
329
|
+
"optional_roles": chain_def.optional_roles,
|
|
330
|
+
"gate": chain_def.gate,
|
|
331
|
+
"gate_description": chain_def.gate_description,
|
|
332
|
+
"anti_skip_message": chain_def.anti_skip_message,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def get_shared_mapper(confidence_threshold: float = 0.3) -> IntentWorkflowMapper:
|
|
337
|
+
"""
|
|
338
|
+
Get or create shared singleton instance.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
confidence_threshold: Minimum confidence to accept intent match
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Shared IntentWorkflowMapper instance
|
|
345
|
+
"""
|
|
346
|
+
if not hasattr(get_shared_mapper, "_instance"):
|
|
347
|
+
get_shared_mapper._instance = IntentWorkflowMapper(
|
|
348
|
+
confidence_threshold=confidence_threshold
|
|
349
|
+
)
|
|
350
|
+
return get_shared_mapper._instance
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import ast
|
|
3
|
+
import re
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, List, Any, Optional
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LanguageParser:
|
|
12
|
+
"""Base class for language-specific code parsers."""
|
|
13
|
+
|
|
14
|
+
def file_patterns(self) -> List[str]:
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
def exclude_patterns(self) -> List[str]:
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
def parse_file(self, source: str, file_path: str) -> Optional[Dict[str, Any]]:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def extract_dependencies(self, source: str) -> List[str]:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def is_available(self) -> bool:
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PythonParser(LanguageParser):
|
|
31
|
+
"""Python AST-based language parser."""
|
|
32
|
+
|
|
33
|
+
def file_patterns(self) -> List[str]:
|
|
34
|
+
return ["*.py"]
|
|
35
|
+
|
|
36
|
+
def exclude_patterns(self) -> List[str]:
|
|
37
|
+
return ["__pycache__", "test_", "_test.py", ".venv"]
|
|
38
|
+
|
|
39
|
+
def parse_file(self, source: str, file_path: str) -> Optional[Dict[str, Any]]:
|
|
40
|
+
try:
|
|
41
|
+
tree = ast.parse(source)
|
|
42
|
+
except (SyntaxError, UnicodeDecodeError) as e:
|
|
43
|
+
logger.debug("Python parse failed for %s: %s", file_path, e)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
file_imports = []
|
|
47
|
+
top_level_nodes = []
|
|
48
|
+
|
|
49
|
+
for node in ast.iter_child_nodes(tree):
|
|
50
|
+
if isinstance(node, ast.Import):
|
|
51
|
+
for alias in node.names:
|
|
52
|
+
file_imports.append(alias.name)
|
|
53
|
+
elif isinstance(node, ast.ImportFrom):
|
|
54
|
+
module = node.module or ""
|
|
55
|
+
file_imports.append(module)
|
|
56
|
+
|
|
57
|
+
if isinstance(node, ast.ClassDef):
|
|
58
|
+
top_level_nodes.append(self._parse_class(node, file_path))
|
|
59
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
60
|
+
top_level_nodes.append(self._parse_function(node, file_path))
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"file": Path(file_path).name,
|
|
64
|
+
"language": "python",
|
|
65
|
+
"imports": file_imports[:20],
|
|
66
|
+
"nodes": top_level_nodes,
|
|
67
|
+
"total_classes": sum(1 for n in top_level_nodes if n["type"] == "class"),
|
|
68
|
+
"total_functions": sum(1 for n in top_level_nodes if n["type"] in ("function", "method")),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def _parse_class(self, node, file_path: str) -> Dict[str, Any]:
|
|
72
|
+
docstring = ast.get_docstring(node) or ""
|
|
73
|
+
methods = []
|
|
74
|
+
for item in node.body:
|
|
75
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
76
|
+
methods.append(self._parse_function(item, file_path))
|
|
77
|
+
return {
|
|
78
|
+
"name": node.name, "type": "class", "file": file_path,
|
|
79
|
+
"lines": f"{node.lineno}-{node.end_lineno or node.lineno}",
|
|
80
|
+
"docstring": docstring[:100], "children": methods,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def _parse_function(self, node, file_path: str) -> Dict[str, Any]:
|
|
84
|
+
docstring = ast.get_docstring(node) or ""
|
|
85
|
+
calls = []
|
|
86
|
+
for child in ast.walk(node):
|
|
87
|
+
if isinstance(child, ast.Call):
|
|
88
|
+
if isinstance(child.func, ast.Name):
|
|
89
|
+
calls.append(child.func.id)
|
|
90
|
+
elif isinstance(child.func, ast.Attribute):
|
|
91
|
+
calls.append(child.func.attr)
|
|
92
|
+
return {
|
|
93
|
+
"name": node.name, "type": "function", "file": file_path,
|
|
94
|
+
"lines": f"{node.lineno}-{node.end_lineno or node.lineno}",
|
|
95
|
+
"docstring": docstring[:100], "calls": list(set(calls))[:10],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def extract_dependencies(self, source: str) -> List[str]:
|
|
99
|
+
try:
|
|
100
|
+
tree = ast.parse(source)
|
|
101
|
+
except (SyntaxError, UnicodeDecodeError) as e:
|
|
102
|
+
logger.debug("Python dependency extraction failed: %s", e)
|
|
103
|
+
return []
|
|
104
|
+
deps = set()
|
|
105
|
+
for node in ast.walk(tree):
|
|
106
|
+
if isinstance(node, ast.ImportFrom) and node.module:
|
|
107
|
+
deps.add(node.module)
|
|
108
|
+
elif isinstance(node, ast.Import):
|
|
109
|
+
for alias in node.names:
|
|
110
|
+
deps.add(alias.name)
|
|
111
|
+
return sorted(deps)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class JavaScriptParser(LanguageParser):
|
|
115
|
+
"""JavaScript/TypeScript regex-based parser."""
|
|
116
|
+
|
|
117
|
+
_CLASS_RE = re.compile(
|
|
118
|
+
r'(?:export\s+)?(?:default\s+)?class\s+(\w+)', re.MULTILINE
|
|
119
|
+
)
|
|
120
|
+
_FUNC_RE = re.compile(
|
|
121
|
+
r'(?:export\s+)?(?:async\s+)?function\s+(\w+)|'
|
|
122
|
+
r'(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>',
|
|
123
|
+
re.MULTILINE,
|
|
124
|
+
)
|
|
125
|
+
_IMPORT_RE = re.compile(
|
|
126
|
+
r"""import\s+.*?\s+from\s+['"]([^'"]+)['"]|"""
|
|
127
|
+
r"""require\s*\(\s*['"]([^'"]+)['"]\s*\)""",
|
|
128
|
+
re.MULTILINE,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def file_patterns(self) -> List[str]:
|
|
132
|
+
return ["*.js", "*.jsx", "*.ts", "*.tsx"]
|
|
133
|
+
|
|
134
|
+
def exclude_patterns(self) -> List[str]:
|
|
135
|
+
return ["node_modules", "dist", ".next", "coverage", "build"]
|
|
136
|
+
|
|
137
|
+
def parse_file(self, source: str, file_path: str) -> Optional[Dict[str, Any]]:
|
|
138
|
+
try:
|
|
139
|
+
classes = []
|
|
140
|
+
for m in self._CLASS_RE.finditer(source):
|
|
141
|
+
classes.append({
|
|
142
|
+
"name": m.group(1), "type": "class", "file": file_path,
|
|
143
|
+
"lines": "", "docstring": "", "children": [],
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
functions = []
|
|
147
|
+
for m in self._FUNC_RE.finditer(source):
|
|
148
|
+
name = m.group(1) or m.group(2)
|
|
149
|
+
if name:
|
|
150
|
+
functions.append({
|
|
151
|
+
"name": name, "type": "function", "file": file_path,
|
|
152
|
+
"lines": "", "docstring": "", "calls": [],
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if not classes and not functions:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"file": Path(file_path).name,
|
|
160
|
+
"language": "javascript",
|
|
161
|
+
"imports": self.extract_dependencies(source)[:20],
|
|
162
|
+
"nodes": classes + functions,
|
|
163
|
+
"total_classes": len(classes),
|
|
164
|
+
"total_functions": len(functions),
|
|
165
|
+
}
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.debug("JavaScript parse failed for %s: %s", file_path, e)
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def extract_dependencies(self, source: str) -> List[str]:
|
|
171
|
+
deps = set()
|
|
172
|
+
for m in self._IMPORT_RE.finditer(source):
|
|
173
|
+
dep = m.group(1) or m.group(2)
|
|
174
|
+
if dep:
|
|
175
|
+
deps.add(dep)
|
|
176
|
+
return sorted(deps)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class GoParser(LanguageParser):
|
|
180
|
+
"""Go regex-based parser."""
|
|
181
|
+
|
|
182
|
+
_FUNC_RE = re.compile(
|
|
183
|
+
r'func\s+(?:\([^)]+\)\s*)?(\w+)\s*\(', re.MULTILINE
|
|
184
|
+
)
|
|
185
|
+
_TYPE_STRUCT_RE = re.compile(
|
|
186
|
+
r'type\s+(\w+)\s+struct\s*\{', re.MULTILINE
|
|
187
|
+
)
|
|
188
|
+
_TYPE_INTERFACE_RE = re.compile(
|
|
189
|
+
r'type\s+(\w+)\s+interface\s*\{', re.MULTILINE
|
|
190
|
+
)
|
|
191
|
+
_IMPORT_RE = re.compile(
|
|
192
|
+
r'import\s+(?:\(\s*([^)]+)\s*\)|"([^"]+)")', re.MULTILINE | re.DOTALL
|
|
193
|
+
)
|
|
194
|
+
_IMPORT_LINE_RE = re.compile(r'"([^"]+)"')
|
|
195
|
+
|
|
196
|
+
def file_patterns(self) -> List[str]:
|
|
197
|
+
return ["*.go"]
|
|
198
|
+
|
|
199
|
+
def exclude_patterns(self) -> List[str]:
|
|
200
|
+
return ["vendor"]
|
|
201
|
+
|
|
202
|
+
def parse_file(self, source: str, file_path: str) -> Optional[Dict[str, Any]]:
|
|
203
|
+
try:
|
|
204
|
+
structs = []
|
|
205
|
+
for m in self._TYPE_STRUCT_RE.finditer(source):
|
|
206
|
+
structs.append({
|
|
207
|
+
"name": m.group(1), "type": "struct", "file": file_path,
|
|
208
|
+
"lines": "", "docstring": "", "children": [],
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
interfaces = []
|
|
212
|
+
for m in self._TYPE_INTERFACE_RE.finditer(source):
|
|
213
|
+
interfaces.append({
|
|
214
|
+
"name": m.group(1), "type": "interface", "file": file_path,
|
|
215
|
+
"lines": "", "docstring": "", "children": [],
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
functions = []
|
|
219
|
+
for m in self._FUNC_RE.finditer(source):
|
|
220
|
+
functions.append({
|
|
221
|
+
"name": m.group(1), "type": "function", "file": file_path,
|
|
222
|
+
"lines": "", "docstring": "", "calls": [],
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
if not structs and not interfaces and not functions:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"file": Path(file_path).name,
|
|
230
|
+
"language": "go",
|
|
231
|
+
"imports": self.extract_dependencies(source)[:20],
|
|
232
|
+
"nodes": structs + interfaces + functions,
|
|
233
|
+
"total_classes": len(structs) + len(interfaces),
|
|
234
|
+
"total_functions": len(functions),
|
|
235
|
+
}
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.debug("Go parse failed for %s: %s", file_path, e)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
def extract_dependencies(self, source: str) -> List[str]:
|
|
241
|
+
deps = set()
|
|
242
|
+
for m in self._IMPORT_RE.finditer(source):
|
|
243
|
+
block = m.group(1)
|
|
244
|
+
single = m.group(2)
|
|
245
|
+
if block:
|
|
246
|
+
for lm in self._IMPORT_LINE_RE.finditer(block):
|
|
247
|
+
deps.add(lm.group(1))
|
|
248
|
+
elif single:
|
|
249
|
+
deps.add(single)
|
|
250
|
+
return sorted(deps)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class NullLanguageParser(LanguageParser):
|
|
254
|
+
"""Null parser for graceful degradation."""
|
|
255
|
+
|
|
256
|
+
def file_patterns(self) -> List[str]:
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
def parse_file(self, source: str, file_path: str) -> Optional[Dict[str, Any]]:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
def extract_dependencies(self, source: str) -> List[str]:
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
def is_available(self) -> bool:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
DEFAULT_PARSERS = [PythonParser(), JavaScriptParser(), GoParser()]
|