super-dev 2.0.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 (61) hide show
  1. super_dev/__init__.py +11 -0
  2. super_dev/analyzer/__init__.py +34 -0
  3. super_dev/analyzer/analyzer.py +440 -0
  4. super_dev/analyzer/detectors.py +511 -0
  5. super_dev/analyzer/models.py +285 -0
  6. super_dev/cli.py +3257 -0
  7. super_dev/config/__init__.py +11 -0
  8. super_dev/config/frontend.py +557 -0
  9. super_dev/config/manager.py +281 -0
  10. super_dev/creators/__init__.py +26 -0
  11. super_dev/creators/creator.py +134 -0
  12. super_dev/creators/document_generator.py +2473 -0
  13. super_dev/creators/frontend_builder.py +371 -0
  14. super_dev/creators/implementation_builder.py +789 -0
  15. super_dev/creators/prompt_generator.py +289 -0
  16. super_dev/creators/requirement_parser.py +354 -0
  17. super_dev/creators/spec_builder.py +195 -0
  18. super_dev/deployers/__init__.py +20 -0
  19. super_dev/deployers/cicd.py +1269 -0
  20. super_dev/deployers/delivery.py +229 -0
  21. super_dev/deployers/migration.py +1032 -0
  22. super_dev/design/__init__.py +74 -0
  23. super_dev/design/aesthetics.py +530 -0
  24. super_dev/design/charts.py +396 -0
  25. super_dev/design/codegen.py +379 -0
  26. super_dev/design/engine.py +528 -0
  27. super_dev/design/generator.py +395 -0
  28. super_dev/design/landing.py +422 -0
  29. super_dev/design/tech_stack.py +524 -0
  30. super_dev/design/tokens.py +269 -0
  31. super_dev/design/ux_guide.py +391 -0
  32. super_dev/exceptions.py +119 -0
  33. super_dev/experts/__init__.py +19 -0
  34. super_dev/experts/service.py +161 -0
  35. super_dev/integrations/__init__.py +7 -0
  36. super_dev/integrations/manager.py +264 -0
  37. super_dev/orchestrator/__init__.py +12 -0
  38. super_dev/orchestrator/engine.py +958 -0
  39. super_dev/orchestrator/experts.py +423 -0
  40. super_dev/orchestrator/knowledge.py +352 -0
  41. super_dev/orchestrator/quality.py +356 -0
  42. super_dev/reviewers/__init__.py +17 -0
  43. super_dev/reviewers/code_review.py +471 -0
  44. super_dev/reviewers/quality_gate.py +964 -0
  45. super_dev/reviewers/redteam.py +881 -0
  46. super_dev/skills/__init__.py +7 -0
  47. super_dev/skills/manager.py +307 -0
  48. super_dev/specs/__init__.py +44 -0
  49. super_dev/specs/generator.py +264 -0
  50. super_dev/specs/manager.py +428 -0
  51. super_dev/specs/models.py +348 -0
  52. super_dev/specs/validator.py +415 -0
  53. super_dev/utils/__init__.py +11 -0
  54. super_dev/utils/logger.py +133 -0
  55. super_dev/web/api.py +1402 -0
  56. super_dev-2.0.0.dist-info/METADATA +252 -0
  57. super_dev-2.0.0.dist-info/RECORD +61 -0
  58. super_dev-2.0.0.dist-info/WHEEL +5 -0
  59. super_dev-2.0.0.dist-info/entry_points.txt +2 -0
  60. super_dev-2.0.0.dist-info/licenses/LICENSE +21 -0
  61. super_dev-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,964 @@
1
+ """
2
+ 质量门禁检查器 - 确保交付物达到质量标准
3
+
4
+ 开发:Excellent(11964948@qq.com)
5
+ 功能:多维度质量评分和门禁检查
6
+ 作用:按场景阈值(或自定义阈值)评估是否通过质量门禁
7
+ 创建时间:2025-12-30
8
+ """
9
+
10
+ import json
11
+ import shutil
12
+ import subprocess # nosec B404
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ from defusedxml import ElementTree
19
+
20
+ from .redteam import RedTeamReport
21
+
22
+
23
+ class CheckStatus(Enum):
24
+ """检查状态"""
25
+ PASSED = "passed"
26
+ FAILED = "failed"
27
+ WARNING = "warning"
28
+
29
+
30
+ @dataclass
31
+ class QualityCheck:
32
+ """质量检查项"""
33
+ name: str
34
+ category: str # documentation, security, performance, testing, code_quality
35
+ description: str
36
+ status: CheckStatus
37
+ score: int # 0-100
38
+ weight: float = 1.0 # 权重,用于计算加权总分
39
+ details: str = ""
40
+
41
+
42
+ @dataclass
43
+ class QualityGateResult:
44
+ """质量门禁结果"""
45
+ passed: bool
46
+ total_score: int
47
+ weighted_score: float
48
+ checks: list[QualityCheck] = field(default_factory=list)
49
+ critical_failures: list[str] = field(default_factory=list)
50
+ recommendations: list[str] = field(default_factory=list)
51
+ scenario: str = "1-N+1" # 场景类型: "0-1" 或 "1-N+1"
52
+
53
+ @property
54
+ def passed_checks(self) -> list[QualityCheck]:
55
+ return [c for c in self.checks if c.status == CheckStatus.PASSED]
56
+
57
+ @property
58
+ def failed_checks(self) -> list[QualityCheck]:
59
+ return [c for c in self.checks if c.status == CheckStatus.FAILED]
60
+
61
+ @property
62
+ def warning_checks(self) -> list[QualityCheck]:
63
+ return [c for c in self.checks if c.status == CheckStatus.WARNING]
64
+
65
+ def to_markdown(self) -> str:
66
+ """生成 Markdown 报告"""
67
+ status_icon = "通过" if self.passed else "未通过"
68
+ status_color = "green" if self.passed else "red"
69
+
70
+ lines = [
71
+ "# 质量门禁报告",
72
+ "",
73
+ f"**场景**: {self.scenario} ({'0-1 新建项目' if self.scenario == '0-1' else '1-N+1 增量开发'})",
74
+ f"**状态**: <span style='color:{status_color}'>{status_icon}</span>",
75
+ f"**总分**: {self.total_score}/100",
76
+ f"**加权分**: {self.weighted_score:.1f}/100",
77
+ "",
78
+ "---",
79
+ "",
80
+ "## 检查结果摘要",
81
+ "",
82
+ f"- 通过: {len(self.passed_checks)} 项",
83
+ f"- 警告: {len(self.warning_checks)} 项",
84
+ f"- 失败: {len(self.failed_checks)} 项",
85
+ "",
86
+ ]
87
+
88
+ if self.critical_failures:
89
+ lines.extend([
90
+ "## 关键失败项",
91
+ "",
92
+ ])
93
+ for failure in self.critical_failures:
94
+ lines.append(f"- {failure}")
95
+ lines.append("")
96
+
97
+ # 按类别分组展示
98
+ categories: dict[str, list[QualityCheck]] = {}
99
+ for check in self.checks:
100
+ if check.category not in categories:
101
+ categories[check.category] = []
102
+ categories[check.category].append(check)
103
+
104
+ lines.extend([
105
+ "## 详细检查结果",
106
+ "",
107
+ ])
108
+
109
+ for category, checks in categories.items():
110
+ lines.extend([
111
+ f"### {category}",
112
+ "",
113
+ "| 检查项 | 状态 | 得分 | 说明 |",
114
+ "|:---|:---:|:---:|:---|",
115
+ ])
116
+
117
+ for check in checks:
118
+ status_icon = "✓" if check.status == CheckStatus.PASSED else "⚠" if check.status == CheckStatus.WARNING else "✗"
119
+ lines.append(
120
+ f"| {check.name} | {status_icon} | {check.score}/100 | {check.description} |"
121
+ )
122
+
123
+ lines.append("")
124
+
125
+ # 改进建议
126
+ if self.recommendations:
127
+ lines.extend([
128
+ "## 改进建议",
129
+ "",
130
+ ])
131
+ for idx, rec in enumerate(self.recommendations, 1):
132
+ lines.append(f"{idx}. {rec}")
133
+ lines.append("")
134
+
135
+ # 下一步行动
136
+ lines.extend([
137
+ "---",
138
+ "",
139
+ "## 下一步行动",
140
+ "",
141
+ ])
142
+
143
+ if self.passed:
144
+ lines.extend([
145
+ "[通过] 质量门禁已通过,可以继续下一步:",
146
+ "",
147
+ "1. 开始编码实现",
148
+ "2. 设置 CI/CD 流水线",
149
+ "3. 部署到测试环境",
150
+ "",
151
+ ])
152
+ else:
153
+ lines.extend([
154
+ "[未通过] 质量门禁未通过,请完成以下操作后重新检查:",
155
+ "",
156
+ ])
157
+
158
+ failed_items = [f"- {c.description}" for c in self.failed_checks]
159
+ lines.extend(failed_items)
160
+ lines.extend([
161
+ "",
162
+ "修复后运行: `super-dev quality --type all`",
163
+ "",
164
+ ])
165
+
166
+ return "\n".join(lines)
167
+
168
+
169
+ class QualityGateChecker:
170
+ """质量门禁检查器"""
171
+
172
+ # 质量门禁阈值
173
+ PASS_THRESHOLD = 80
174
+ WARNING_THRESHOLD = 60
175
+ # 0-1 场景与增量场景统一使用 80+ 标准
176
+ PASS_THRESHOLD_ZERO_TO_ONE = 80
177
+
178
+ # 检查项配置
179
+ CHECKS_CONFIG = {
180
+ "documentation": {
181
+ "weight": 1.0,
182
+ "required": True,
183
+ },
184
+ "security": {
185
+ "weight": 1.5, # 安全更重要
186
+ "required": True,
187
+ },
188
+ "performance": {
189
+ "weight": 1.2,
190
+ "required": True,
191
+ },
192
+ "testing": {
193
+ "weight": 1.3,
194
+ "required": True,
195
+ },
196
+ "code_quality": {
197
+ "weight": 1.0,
198
+ "required": False,
199
+ },
200
+ }
201
+
202
+ def __init__(
203
+ self,
204
+ project_dir: Path,
205
+ name: str,
206
+ tech_stack: dict,
207
+ scenario_override: str | None = None,
208
+ threshold_override: int | None = None,
209
+ ):
210
+ self.project_dir = Path(project_dir).resolve()
211
+ self.name = name
212
+ self.tech_stack = tech_stack
213
+ self.threshold_override = threshold_override
214
+ if scenario_override in {"0-1", "1-N+1"}:
215
+ self.is_zero_to_one = scenario_override == "0-1"
216
+ else:
217
+ self.is_zero_to_one = self._detect_zero_to_one_scenario()
218
+
219
+ def _detect_zero_to_one_scenario(self) -> bool:
220
+ """
221
+ 检测是否为 0-1 场景(空项目/新建项目)
222
+
223
+ 0-1 场景特征:
224
+ - 没有源代码目录(src/, lib/, app/, server/, client/ 等)
225
+ - 没有配置文件(package.json, requirements.txt, go.mod 等)
226
+ - 只有 output/ 目录(刚生成的文档)
227
+
228
+ Returns:
229
+ bool: True 表示 0-1 场景,False 表示 1-N+1 场景
230
+ """
231
+ # 检查常见的源代码目录
232
+ source_dirs = [
233
+ "src", "lib", "app", "server", "client",
234
+ "backend", "frontend", "api", "handlers",
235
+ "models", "views", "controllers", "services"
236
+ ]
237
+
238
+ has_source_code = any(
239
+ (self.project_dir / d).exists()
240
+ for d in source_dirs
241
+ )
242
+
243
+ # 检查是否有项目配置文件(表明这不是空项目)
244
+ config_files = [
245
+ "package.json", "requirements.txt", "go.mod",
246
+ "Cargo.toml", "pom.xml", "build.gradle"
247
+ ]
248
+
249
+ has_project_config = any(
250
+ (self.project_dir / f).exists()
251
+ for f in config_files
252
+ )
253
+
254
+ # 如果有源代码或有项目配置,说明不是 0-1 场景
255
+ return not (has_source_code or has_project_config)
256
+
257
+ def check(self, redteam_report: Optional["RedTeamReport"] = None) -> QualityGateResult:
258
+ """执行质量门禁检查"""
259
+ checks: list[QualityCheck] = []
260
+
261
+ # 1. 文档质量检查
262
+ checks.extend(self._check_documentation())
263
+
264
+ # 2. 安全检查 (基于红队报告)
265
+ checks.extend(self._check_security(redteam_report))
266
+
267
+ # 3. 性能检查 (基于红队报告)
268
+ checks.extend(self._check_performance(redteam_report))
269
+
270
+ # 4. 测试检查
271
+ checks.extend(self._check_testing())
272
+
273
+ # 5. 代码质量检查
274
+ checks.extend(self._check_code_quality())
275
+
276
+ # 计算总分和加权分
277
+ total_score = self._calculate_total_score(checks)
278
+ weighted_score = self._calculate_weighted_score(checks)
279
+
280
+ # 根据场景选择阈值
281
+ threshold = (
282
+ self.threshold_override
283
+ if self.threshold_override is not None
284
+ else (self.PASS_THRESHOLD_ZERO_TO_ONE if self.is_zero_to_one else self.PASS_THRESHOLD)
285
+ )
286
+
287
+ # 收集关键失败项
288
+ critical_failures = []
289
+ for check in checks:
290
+ config = self.CHECKS_CONFIG.get(check.category, {})
291
+ if config.get("required", False) and check.status == CheckStatus.FAILED:
292
+ critical_failures.append(f"[{check.category}] {check.description}")
293
+
294
+ # 检查是否通过:必须达到阈值,且必检项不能失败
295
+ passed = total_score >= threshold and not critical_failures
296
+
297
+ # 生成改进建议
298
+ recommendations = self._generate_recommendations(checks)
299
+
300
+ # 确定场景类型
301
+ scenario = "0-1" if self.is_zero_to_one else "1-N+1"
302
+
303
+ return QualityGateResult(
304
+ passed=passed,
305
+ total_score=total_score,
306
+ weighted_score=weighted_score,
307
+ checks=checks,
308
+ critical_failures=critical_failures,
309
+ recommendations=recommendations,
310
+ scenario=scenario,
311
+ )
312
+
313
+ def _check_documentation(self) -> list[QualityCheck]:
314
+ """检查文档质量"""
315
+ checks = []
316
+
317
+ # 检查 PRD 是否存在
318
+ prd_path = self.project_dir / "output" / f"{self.name}-prd.md"
319
+ if prd_path.exists():
320
+ content = prd_path.read_text(encoding="utf-8")
321
+ # 简单检查文档完整性
322
+ has_vision = "产品愿景" in content or "vision" in content.lower()
323
+ has_features = "功能需求" in content or "features" in content.lower()
324
+ has_acceptance = "验收标准" in content or "acceptance" in content.lower()
325
+
326
+ score = 100 if has_vision and has_features and has_acceptance else 70
327
+ status = CheckStatus.PASSED if score >= 80 else CheckStatus.WARNING
328
+
329
+ checks.append(QualityCheck(
330
+ name="PRD 文档",
331
+ category="documentation",
332
+ description="产品需求文档完整性",
333
+ status=status,
334
+ score=score,
335
+ weight=self.CHECKS_CONFIG["documentation"]["weight"],
336
+ details="包含产品愿景、功能需求和验收标准" if status == CheckStatus.PASSED else "文档内容不完整",
337
+ ))
338
+ else:
339
+ checks.append(QualityCheck(
340
+ name="PRD 文档",
341
+ category="documentation",
342
+ description="产品需求文档存在性",
343
+ status=CheckStatus.FAILED,
344
+ score=0,
345
+ weight=self.CHECKS_CONFIG["documentation"]["weight"],
346
+ details="PRD 文档不存在",
347
+ ))
348
+
349
+ # 检查架构文档是否存在
350
+ arch_path = self.project_dir / "output" / f"{self.name}-architecture.md"
351
+ if arch_path.exists():
352
+ content = arch_path.read_text(encoding="utf-8")
353
+ has_tech_stack = "技术栈" in content or "tech stack" in content.lower()
354
+ has_database = "数据库" in content or "database" in content.lower()
355
+ has_api = "API" in content
356
+
357
+ score = 100 if has_tech_stack and has_database and has_api else 70
358
+ status = CheckStatus.PASSED if score >= 80 else CheckStatus.WARNING
359
+
360
+ checks.append(QualityCheck(
361
+ name="架构文档",
362
+ category="documentation",
363
+ description="架构设计文档完整性",
364
+ status=status,
365
+ score=score,
366
+ weight=self.CHECKS_CONFIG["documentation"]["weight"],
367
+ details="包含技术栈、数据库设计和 API 设计" if status == CheckStatus.PASSED else "文档内容不完整",
368
+ ))
369
+ else:
370
+ checks.append(QualityCheck(
371
+ name="架构文档",
372
+ category="documentation",
373
+ description="架构设计文档存在性",
374
+ status=CheckStatus.FAILED,
375
+ score=0,
376
+ weight=self.CHECKS_CONFIG["documentation"]["weight"],
377
+ details="架构文档不存在",
378
+ ))
379
+
380
+ # 检查 UI/UX 文档是否存在
381
+ uiux_path = self.project_dir / "output" / f"{self.name}-uiux.md"
382
+ if uiux_path.exists():
383
+ checks.append(QualityCheck(
384
+ name="UI/UX 文档",
385
+ category="documentation",
386
+ description="UI/UX 设计文档存在性",
387
+ status=CheckStatus.PASSED,
388
+ score=100,
389
+ weight=self.CHECKS_CONFIG["documentation"]["weight"],
390
+ details="UI/UX 文档已创建",
391
+ ))
392
+ else:
393
+ checks.append(QualityCheck(
394
+ name="UI/UX 文档",
395
+ category="documentation",
396
+ description="UI/UX 设计文档存在性",
397
+ status=CheckStatus.WARNING,
398
+ score=50,
399
+ weight=self.CHECKS_CONFIG["documentation"]["weight"],
400
+ details="UI/UX 文档不存在(可选)",
401
+ ))
402
+
403
+ return checks
404
+
405
+ def _check_security(self, redteam_report: RedTeamReport | None) -> list[QualityCheck]:
406
+ """检查安全性"""
407
+ checks: list[QualityCheck] = []
408
+
409
+ if redteam_report:
410
+ critical_count = sum(1 for i in redteam_report.security_issues if i.severity == "critical")
411
+ high_count = sum(1 for i in redteam_report.security_issues if i.severity == "high")
412
+
413
+ if critical_count > 0:
414
+ score = max(0, 100 - critical_count * 30)
415
+ status = CheckStatus.FAILED
416
+ elif high_count > 2:
417
+ score = max(0, 100 - high_count * 15)
418
+ status = CheckStatus.WARNING
419
+ else:
420
+ score = 100
421
+ status = CheckStatus.PASSED
422
+
423
+ checks.append(QualityCheck(
424
+ name="安全审查",
425
+ category="security",
426
+ description=f"安全检查 ({critical_count} critical, {high_count} high)",
427
+ status=status,
428
+ score=score,
429
+ weight=self.CHECKS_CONFIG["security"]["weight"],
430
+ details=f"发现 {critical_count} 个严重问题和 {high_count} 个高危问题" if critical_count + high_count > 0 else "未发现严重安全问题",
431
+ ))
432
+ else:
433
+ # 未进行红队审查,给警告
434
+ checks.append(QualityCheck(
435
+ name="安全审查",
436
+ category="security",
437
+ description="安全检查状态",
438
+ status=CheckStatus.WARNING,
439
+ score=50,
440
+ weight=self.CHECKS_CONFIG["security"]["weight"],
441
+ details="未进行红队安全审查",
442
+ ))
443
+
444
+ return checks
445
+
446
+ def _check_performance(self, redteam_report: RedTeamReport | None) -> list[QualityCheck]:
447
+ """检查性能"""
448
+ checks: list[QualityCheck] = []
449
+
450
+ if redteam_report:
451
+ critical_count = sum(1 for i in redteam_report.performance_issues if i.severity == "critical")
452
+ high_count = sum(1 for i in redteam_report.performance_issues if i.severity == "high")
453
+
454
+ if critical_count > 0:
455
+ score = max(0, 100 - critical_count * 25)
456
+ status = CheckStatus.FAILED
457
+ elif high_count > 2:
458
+ score = max(0, 100 - high_count * 10)
459
+ status = CheckStatus.WARNING
460
+ else:
461
+ score = 100
462
+ status = CheckStatus.PASSED
463
+
464
+ checks.append(QualityCheck(
465
+ name="性能审查",
466
+ category="performance",
467
+ description=f"性能检查 ({critical_count} critical, {high_count} high)",
468
+ status=status,
469
+ score=score,
470
+ weight=self.CHECKS_CONFIG["performance"]["weight"],
471
+ details=f"发现 {critical_count} 个严重问题和 {high_count} 个高危问题" if critical_count + high_count > 0 else "未发现严重性能问题",
472
+ ))
473
+ else:
474
+ checks.append(QualityCheck(
475
+ name="性能审查",
476
+ category="performance",
477
+ description="性能检查状态",
478
+ status=CheckStatus.WARNING,
479
+ score=50,
480
+ weight=self.CHECKS_CONFIG["performance"]["weight"],
481
+ details="未进行红队性能审查",
482
+ ))
483
+
484
+ return checks
485
+
486
+ def _check_testing(self) -> list[QualityCheck]:
487
+ """检查测试策略"""
488
+ checks: list[QualityCheck] = []
489
+
490
+ # 检查是否有测试配置
491
+ has_jest = self._has_js_test_script()
492
+ has_pytest = self._has_pytest_config()
493
+
494
+ if has_jest or has_pytest:
495
+ checks.append(QualityCheck(
496
+ name="测试框架",
497
+ category="testing",
498
+ description="测试框架配置",
499
+ status=CheckStatus.PASSED,
500
+ score=100,
501
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
502
+ details="测试框架已配置",
503
+ ))
504
+ else:
505
+ checks.append(QualityCheck(
506
+ name="测试框架",
507
+ category="testing",
508
+ description="测试框架配置",
509
+ status=CheckStatus.WARNING,
510
+ score=50,
511
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
512
+ details="测试框架未配置",
513
+ ))
514
+
515
+ python_tests = self._discover_python_tests()
516
+ js_test_targets = self._discover_js_test_targets()
517
+
518
+ # 真实测试执行检查(优先 Python)
519
+ if python_tests:
520
+ pytest_executable = shutil.which("pytest")
521
+ if pytest_executable:
522
+ result = self._run_command(
523
+ [pytest_executable, "-q", "--maxfail=1"],
524
+ timeout=180,
525
+ )
526
+ if result["timed_out"]:
527
+ checks.append(QualityCheck(
528
+ name="测试执行",
529
+ category="testing",
530
+ description="自动化测试执行结果",
531
+ status=CheckStatus.WARNING,
532
+ score=40,
533
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
534
+ details="pytest 执行超时,建议拆分测试或优化测试速度",
535
+ ))
536
+ elif result["returncode"] == 0:
537
+ summary = self._extract_test_summary(str(result["stdout"]))
538
+ checks.append(QualityCheck(
539
+ name="测试执行",
540
+ category="testing",
541
+ description="自动化测试执行结果",
542
+ status=CheckStatus.PASSED,
543
+ score=100,
544
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
545
+ details=summary or "pytest 执行通过",
546
+ ))
547
+ else:
548
+ summary = self._extract_test_summary(str(result["stdout"] or result["stderr"]))
549
+ checks.append(QualityCheck(
550
+ name="测试执行",
551
+ category="testing",
552
+ description="自动化测试执行结果",
553
+ status=CheckStatus.FAILED,
554
+ score=20,
555
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
556
+ details=summary or "pytest 执行失败",
557
+ ))
558
+ else:
559
+ checks.append(QualityCheck(
560
+ name="测试执行",
561
+ category="testing",
562
+ description="自动化测试执行结果",
563
+ status=CheckStatus.WARNING,
564
+ score=40,
565
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
566
+ details="检测到 Python 测试,但未找到 pytest 可执行文件",
567
+ ))
568
+ elif js_test_targets:
569
+ npm_executable = shutil.which("npm")
570
+ if not npm_executable:
571
+ checks.append(QualityCheck(
572
+ name="测试执行",
573
+ category="testing",
574
+ description="自动化测试执行结果",
575
+ status=CheckStatus.WARNING,
576
+ score=40,
577
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
578
+ details="检测到 JS 测试脚本,但未找到 npm 可执行文件",
579
+ ))
580
+ else:
581
+ timed_out_targets: list[str] = []
582
+ failed_targets: list[str] = []
583
+ passed_targets: list[str] = []
584
+ for target in js_test_targets:
585
+ rel = "."
586
+ if target != self.project_dir:
587
+ rel = str(target.relative_to(self.project_dir))
588
+ result = self._run_command(
589
+ [
590
+ npm_executable,
591
+ "--prefix",
592
+ str(target),
593
+ "run",
594
+ "test",
595
+ "--if-present",
596
+ ],
597
+ timeout=240,
598
+ )
599
+ if result["timed_out"]:
600
+ timed_out_targets.append(rel)
601
+ elif result["returncode"] == 0:
602
+ passed_targets.append(rel)
603
+ else:
604
+ failed_targets.append(rel)
605
+
606
+ if failed_targets:
607
+ checks.append(QualityCheck(
608
+ name="测试执行",
609
+ category="testing",
610
+ description="自动化测试执行结果",
611
+ status=CheckStatus.FAILED,
612
+ score=20,
613
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
614
+ details=f"JS 测试失败: {', '.join(failed_targets)}",
615
+ ))
616
+ elif timed_out_targets:
617
+ checks.append(QualityCheck(
618
+ name="测试执行",
619
+ category="testing",
620
+ description="自动化测试执行结果",
621
+ status=CheckStatus.WARNING,
622
+ score=40,
623
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
624
+ details=f"JS 测试超时: {', '.join(timed_out_targets)}",
625
+ ))
626
+ else:
627
+ checks.append(QualityCheck(
628
+ name="测试执行",
629
+ category="testing",
630
+ description="自动化测试执行结果",
631
+ status=CheckStatus.PASSED,
632
+ score=100,
633
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
634
+ details=f"JS 测试通过: {', '.join(passed_targets)}",
635
+ ))
636
+ else:
637
+ warning_score = 70 if self.is_zero_to_one else 40
638
+ checks.append(QualityCheck(
639
+ name="测试执行",
640
+ category="testing",
641
+ description="自动化测试执行结果",
642
+ status=CheckStatus.WARNING,
643
+ score=warning_score,
644
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
645
+ details="未检测到可执行测试用例",
646
+ ))
647
+
648
+ coverage_percent = self._read_coverage_percent()
649
+ if coverage_percent is None:
650
+ warning_score = 70 if self.is_zero_to_one else 50
651
+ checks.append(QualityCheck(
652
+ name="测试覆盖率",
653
+ category="testing",
654
+ description="覆盖率报告",
655
+ status=CheckStatus.WARNING,
656
+ score=warning_score,
657
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
658
+ details="未检测到 coverage.xml 报告",
659
+ ))
660
+ elif coverage_percent >= 80:
661
+ checks.append(QualityCheck(
662
+ name="测试覆盖率",
663
+ category="testing",
664
+ description="覆盖率报告",
665
+ status=CheckStatus.PASSED,
666
+ score=coverage_percent,
667
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
668
+ details=f"覆盖率 {coverage_percent}%",
669
+ ))
670
+ elif coverage_percent >= 60:
671
+ checks.append(QualityCheck(
672
+ name="测试覆盖率",
673
+ category="testing",
674
+ description="覆盖率报告",
675
+ status=CheckStatus.WARNING,
676
+ score=coverage_percent,
677
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
678
+ details=f"覆盖率 {coverage_percent}%(建议提升到 80%+)",
679
+ ))
680
+ else:
681
+ checks.append(QualityCheck(
682
+ name="测试覆盖率",
683
+ category="testing",
684
+ description="覆盖率报告",
685
+ status=CheckStatus.FAILED,
686
+ score=coverage_percent,
687
+ weight=self.CHECKS_CONFIG["testing"]["weight"],
688
+ details=f"覆盖率 {coverage_percent}%(低于最低建议)",
689
+ ))
690
+
691
+ return checks
692
+
693
+ def _check_code_quality(self) -> list[QualityCheck]:
694
+ """检查代码质量工具"""
695
+ checks: list[QualityCheck] = []
696
+
697
+ # 检查 Linter
698
+ has_eslint = (self.project_dir / ".eslintrc.js").exists() or (
699
+ self.project_dir / ".eslintrc.json"
700
+ ).exists()
701
+ has_pylint = (self.project_dir / "pylint.ini").exists()
702
+ has_black = (self.project_dir / "pyproject.toml").exists() and "black" in (
703
+ self.project_dir / "pyproject.toml"
704
+ ).read_text(encoding='utf-8')
705
+
706
+ if has_eslint or has_pylint or has_black:
707
+ checks.append(QualityCheck(
708
+ name="Linter",
709
+ category="code_quality",
710
+ description="代码静态检查工具",
711
+ status=CheckStatus.PASSED,
712
+ score=100,
713
+ weight=self.CHECKS_CONFIG["code_quality"]["weight"],
714
+ details="Linter 已配置",
715
+ ))
716
+ else:
717
+ checks.append(QualityCheck(
718
+ name="Linter",
719
+ category="code_quality",
720
+ description="代码静态检查工具",
721
+ status=CheckStatus.WARNING,
722
+ score=50,
723
+ weight=self.CHECKS_CONFIG["code_quality"]["weight"],
724
+ details="Linter 未配置",
725
+ ))
726
+
727
+ python_roots = self._discover_python_source_roots()
728
+ if python_roots:
729
+ python_exec = shutil.which("python3") or shutil.which("python")
730
+ if python_exec:
731
+ cmd = [python_exec, "-m", "compileall", "-q", *[str(p) for p in python_roots]]
732
+ result = self._run_command(cmd, timeout=120)
733
+ if result["timed_out"]:
734
+ checks.append(QualityCheck(
735
+ name="Python 语法检查",
736
+ category="code_quality",
737
+ description="compileall 语法检查",
738
+ status=CheckStatus.WARNING,
739
+ score=50,
740
+ weight=self.CHECKS_CONFIG["code_quality"]["weight"],
741
+ details="compileall 执行超时",
742
+ ))
743
+ elif result["returncode"] == 0:
744
+ checks.append(QualityCheck(
745
+ name="Python 语法检查",
746
+ category="code_quality",
747
+ description="compileall 语法检查",
748
+ status=CheckStatus.PASSED,
749
+ score=100,
750
+ weight=self.CHECKS_CONFIG["code_quality"]["weight"],
751
+ details="Python 语法检查通过",
752
+ ))
753
+ else:
754
+ checks.append(QualityCheck(
755
+ name="Python 语法检查",
756
+ category="code_quality",
757
+ description="compileall 语法检查",
758
+ status=CheckStatus.FAILED,
759
+ score=20,
760
+ weight=self.CHECKS_CONFIG["code_quality"]["weight"],
761
+ details="Python 语法检查失败",
762
+ ))
763
+ else:
764
+ checks.append(QualityCheck(
765
+ name="Python 语法检查",
766
+ category="code_quality",
767
+ description="compileall 语法检查",
768
+ status=CheckStatus.WARNING,
769
+ score=50,
770
+ weight=self.CHECKS_CONFIG["code_quality"]["weight"],
771
+ details="未找到 python 解释器,跳过语法检查",
772
+ ))
773
+
774
+ return checks
775
+
776
+ def _calculate_total_score(self, checks: list[QualityCheck]) -> int:
777
+ """计算总分"""
778
+ if not checks:
779
+ return 0
780
+
781
+ return sum(c.score for c in checks) // len(checks)
782
+
783
+ def _calculate_weighted_score(self, checks: list[QualityCheck]) -> float:
784
+ """计算加权分"""
785
+ if not checks:
786
+ return 0.0
787
+
788
+ total_weight = sum(c.weight for c in checks)
789
+ if total_weight == 0:
790
+ return 0.0
791
+
792
+ weighted_sum = sum(c.score * c.weight for c in checks)
793
+ return weighted_sum / total_weight
794
+
795
+ def _generate_recommendations(self, checks: list[QualityCheck]) -> list[str]:
796
+ """生成改进建议"""
797
+ recommendations: list[str] = []
798
+
799
+ for check in checks:
800
+ if check.status == CheckStatus.FAILED:
801
+ recommendations.append(f"修复: {check.description}")
802
+ elif check.status == CheckStatus.WARNING:
803
+ recommendations.append(f"建议: {check.description}")
804
+
805
+ return recommendations
806
+
807
+ def _discover_python_tests(self) -> list[Path]:
808
+ roots = [self.project_dir / "tests", self.project_dir / "backend" / "tests"]
809
+ files: list[Path] = []
810
+ for tests_dir in roots:
811
+ if not tests_dir.exists():
812
+ continue
813
+ files.extend(list(tests_dir.rglob("test_*.py")))
814
+ files.extend(list(tests_dir.rglob("*_test.py")))
815
+ return files
816
+
817
+ def _has_pytest_config(self) -> bool:
818
+ if (self.project_dir / "pytest.ini").exists():
819
+ return True
820
+
821
+ setup_cfg = self.project_dir / "setup.cfg"
822
+ if setup_cfg.exists():
823
+ content = setup_cfg.read_text(encoding="utf-8", errors="ignore")
824
+ if "[tool:pytest]" in content:
825
+ return True
826
+
827
+ tox_ini = self.project_dir / "tox.ini"
828
+ if tox_ini.exists():
829
+ content = tox_ini.read_text(encoding="utf-8", errors="ignore")
830
+ if "[pytest]" in content:
831
+ return True
832
+
833
+ pyproject = self.project_dir / "pyproject.toml"
834
+ if pyproject.exists():
835
+ content = pyproject.read_text(encoding="utf-8", errors="ignore")
836
+ if "pytest.ini_options" in content:
837
+ return True
838
+
839
+ backend_pyproject = self.project_dir / "backend" / "pyproject.toml"
840
+ if backend_pyproject.exists():
841
+ content = backend_pyproject.read_text(encoding="utf-8", errors="ignore")
842
+ if "pytest.ini_options" in content:
843
+ return True
844
+
845
+ return False
846
+
847
+ def _discover_python_source_roots(self) -> list[Path]:
848
+ roots: list[Path] = []
849
+ candidates = [
850
+ "super_dev", "src", "app", "backend", "server", "api", "services", "lib"
851
+ ]
852
+ for name in candidates:
853
+ path = self.project_dir / name
854
+ if path.exists() and path.is_dir():
855
+ roots.append(path)
856
+
857
+ top_level_py = list(self.project_dir.glob("*.py"))
858
+ roots.extend(top_level_py)
859
+
860
+ # 去重并限制数量,避免无界扫描
861
+ unique: list[Path] = []
862
+ seen = set()
863
+ for path in roots:
864
+ key = str(path.resolve())
865
+ if key in seen:
866
+ continue
867
+ seen.add(key)
868
+ unique.append(path)
869
+ return unique[:8]
870
+
871
+ def _discover_js_test_targets(self) -> list[Path]:
872
+ targets: list[Path] = []
873
+ for root in (self.project_dir, self.project_dir / "frontend", self.project_dir / "backend"):
874
+ package_json = root / "package.json"
875
+ if not package_json.exists():
876
+ continue
877
+ try:
878
+ data = json.loads(package_json.read_text(encoding="utf-8"))
879
+ except Exception:
880
+ continue
881
+ scripts = data.get("scripts", {})
882
+ test_script = str(scripts.get("test", "")).strip()
883
+ if not test_script:
884
+ continue
885
+ if test_script == 'echo "Error: no test specified" && exit 1':
886
+ continue
887
+ targets.append(root)
888
+ return targets
889
+
890
+ def _has_js_test_script(self) -> bool:
891
+ return bool(self._discover_js_test_targets())
892
+
893
+ def _extract_test_summary(self, output: str) -> str:
894
+ if not output:
895
+ return ""
896
+ lines = [line.strip() for line in output.splitlines() if line.strip()]
897
+ for line in reversed(lines):
898
+ if "passed" in line or "failed" in line or "error" in line:
899
+ return line[:240]
900
+ return lines[-1][:240] if lines else ""
901
+
902
+ def _read_coverage_percent(self) -> int | None:
903
+ coverage_candidates = [
904
+ self.project_dir / "coverage.xml",
905
+ self.project_dir / "backend" / "coverage.xml",
906
+ self.project_dir / "frontend" / "coverage.xml",
907
+ self.project_dir / "coverage" / "cobertura-coverage.xml",
908
+ self.project_dir / "frontend" / "coverage" / "cobertura-coverage.xml",
909
+ self.project_dir / "backend" / "coverage" / "cobertura-coverage.xml",
910
+ ]
911
+
912
+ parsed_values: list[int] = []
913
+ for coverage_xml in coverage_candidates:
914
+ if not coverage_xml.exists():
915
+ continue
916
+ try:
917
+ root = ElementTree.fromstring(coverage_xml.read_text(encoding="utf-8"))
918
+ line_rate = root.attrib.get("line-rate")
919
+ if line_rate is not None:
920
+ parsed_values.append(max(0, min(100, int(round(float(line_rate) * 100)))))
921
+ continue
922
+
923
+ lines_covered = root.attrib.get("lines-covered")
924
+ lines_valid = root.attrib.get("lines-valid")
925
+ if lines_covered and lines_valid and float(lines_valid) > 0:
926
+ percent = (float(lines_covered) / float(lines_valid)) * 100
927
+ parsed_values.append(max(0, min(100, int(round(percent)))))
928
+ except Exception:
929
+ continue
930
+
931
+ if not parsed_values:
932
+ return None
933
+ return max(parsed_values)
934
+
935
+ def _run_command(self, cmd: list[str], timeout: int = 120) -> dict[str, object]:
936
+ try:
937
+ completed = subprocess.run(
938
+ cmd,
939
+ cwd=str(self.project_dir),
940
+ capture_output=True,
941
+ text=True,
942
+ timeout=timeout,
943
+ check=False,
944
+ ) # nosec B603
945
+ return {
946
+ "returncode": completed.returncode,
947
+ "stdout": completed.stdout or "",
948
+ "stderr": completed.stderr or "",
949
+ "timed_out": False,
950
+ }
951
+ except subprocess.TimeoutExpired as e:
952
+ return {
953
+ "returncode": -1,
954
+ "stdout": (e.stdout or ""),
955
+ "stderr": (e.stderr or ""),
956
+ "timed_out": True,
957
+ }
958
+ except Exception as e:
959
+ return {
960
+ "returncode": -1,
961
+ "stdout": "",
962
+ "stderr": str(e),
963
+ "timed_out": False,
964
+ }