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,352 @@
1
+ """
2
+ 知识增强模块
3
+
4
+ 将用户输入需求通过「本地知识库 + 联网检索」进行增强,
5
+ 为后续 PRD / 架构 / UIUX / Spec 生成提供更完整上下文。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import urllib.parse
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import requests # type: ignore[import-untyped]
18
+
19
+
20
+ @dataclass
21
+ class KnowledgeItem:
22
+ """知识项"""
23
+
24
+ source: str
25
+ title: str
26
+ snippet: str
27
+ link: str = ""
28
+ score: float = 0.0
29
+
30
+ def to_dict(self) -> dict[str, Any]:
31
+ return {
32
+ "source": self.source,
33
+ "title": self.title,
34
+ "snippet": self.snippet,
35
+ "link": self.link,
36
+ "score": self.score,
37
+ }
38
+
39
+
40
+ class KnowledgeAugmenter:
41
+ """需求知识增强器"""
42
+
43
+ _STOPWORDS = {
44
+ "the",
45
+ "and",
46
+ "for",
47
+ "with",
48
+ "that",
49
+ "from",
50
+ "this",
51
+ "功能",
52
+ "项目",
53
+ "系统",
54
+ "需要",
55
+ "支持",
56
+ "实现",
57
+ "一个",
58
+ "需求",
59
+ }
60
+
61
+ def __init__(self, project_dir: Path, web_enabled: bool = True):
62
+ self.project_dir = Path(project_dir).resolve()
63
+ self.web_enabled = web_enabled
64
+ self.docs_dir = self.project_dir / "docs"
65
+ self.specs_dir = self.project_dir / ".super-dev" / "specs"
66
+ self.data_dir = self.project_dir / "super_dev" / "data"
67
+ self.builtin_data_dir = Path(__file__).resolve().parents[1] / "data"
68
+
69
+ def augment(
70
+ self,
71
+ requirement: str,
72
+ domain: str = "",
73
+ max_local_results: int = 8,
74
+ max_web_results: int = 5,
75
+ ) -> dict[str, Any]:
76
+ """对需求做知识增强"""
77
+ query = requirement.strip()
78
+ keywords = self._extract_keywords(query)
79
+
80
+ local_items = self._collect_local_items(
81
+ keywords=keywords,
82
+ max_results=max_local_results,
83
+ )
84
+ web_items = self._collect_web_items(
85
+ query=self._build_web_query(query, domain),
86
+ max_results=max_web_results,
87
+ )
88
+
89
+ enriched_requirement = self._compose_enriched_requirement(
90
+ requirement=requirement,
91
+ local_items=local_items,
92
+ web_items=web_items,
93
+ )
94
+
95
+ return {
96
+ "original_requirement": requirement,
97
+ "domain": domain,
98
+ "keywords": keywords,
99
+ "local_knowledge": [item.to_dict() for item in local_items],
100
+ "web_knowledge": [item.to_dict() for item in web_items],
101
+ "enriched_requirement": enriched_requirement,
102
+ }
103
+
104
+ def to_markdown(self, bundle: dict[str, Any]) -> str:
105
+ """将增强结果渲染为 Markdown 报告"""
106
+ lines = [
107
+ "# 需求增强报告",
108
+ "",
109
+ f"**原始需求**: {bundle.get('original_requirement', '')}",
110
+ f"**领域**: {bundle.get('domain', 'general') or 'general'}",
111
+ "",
112
+ "## 提取关键词",
113
+ "",
114
+ ", ".join(bundle.get("keywords", [])) or "(none)",
115
+ "",
116
+ "## 本地知识库结果",
117
+ "",
118
+ ]
119
+
120
+ local_items = bundle.get("local_knowledge", [])
121
+ if not local_items:
122
+ lines.append("- 未命中本地知识。")
123
+ else:
124
+ for item in local_items:
125
+ title = item.get("title", "unknown")
126
+ snippet = item.get("snippet", "")
127
+ lines.append(f"- **{title}** ({item.get('source', 'local')}): {snippet}")
128
+ lines.append("")
129
+
130
+ lines.extend(["## 联网检索结果", ""])
131
+ web_items = bundle.get("web_knowledge", [])
132
+ if not web_items:
133
+ lines.append("- 未获得联网结果(可能网络受限或无匹配结果)。")
134
+ else:
135
+ for item in web_items:
136
+ title = item.get("title", "unknown")
137
+ snippet = item.get("snippet", "")
138
+ link = item.get("link", "")
139
+ link_text = f" [{link}]({link})" if link else ""
140
+ lines.append(f"- **{title}**: {snippet}{link_text}")
141
+ lines.append("")
142
+
143
+ lines.extend(
144
+ [
145
+ "## 增强后的需求描述",
146
+ "",
147
+ bundle.get("enriched_requirement", bundle.get("original_requirement", "")),
148
+ "",
149
+ ]
150
+ )
151
+ return "\n".join(lines)
152
+
153
+ def _extract_keywords(self, text: str) -> list[str]:
154
+ tokens = re.findall(r"[A-Za-z0-9_\u4e00-\u9fff]{2,}", text.lower())
155
+ expanded: list[str] = []
156
+ for token in tokens:
157
+ expanded.append(token)
158
+ # 对纯中文短句追加 2 字滑窗关键词,提升匹配率
159
+ if re.fullmatch(r"[\u4e00-\u9fff]{3,}", token):
160
+ for i in range(len(token) - 1):
161
+ expanded.append(token[i : i + 2])
162
+
163
+ unique: list[str] = []
164
+ for token in expanded:
165
+ if token in self._STOPWORDS:
166
+ continue
167
+ if token not in unique:
168
+ unique.append(token)
169
+ return unique[:12]
170
+
171
+ def _iter_local_files(self) -> list[Path]:
172
+ files: list[Path] = []
173
+ if self.docs_dir.exists():
174
+ files.extend(self.docs_dir.rglob("*.md"))
175
+ if self.specs_dir.exists():
176
+ files.extend(self.specs_dir.rglob("*.md"))
177
+ if self.data_dir.exists():
178
+ files.extend(self.data_dir.rglob("*.csv"))
179
+ if self.builtin_data_dir.exists() and self.builtin_data_dir != self.data_dir:
180
+ files.extend(self.builtin_data_dir.rglob("*.csv"))
181
+ return files
182
+
183
+ def _collect_local_items(self, keywords: list[str], max_results: int) -> list[KnowledgeItem]:
184
+ if not keywords:
185
+ return []
186
+
187
+ items: list[KnowledgeItem] = []
188
+ for file_path in self._iter_local_files():
189
+ content = ""
190
+ try:
191
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
192
+ except Exception:
193
+ content = ""
194
+ if not content:
195
+ continue
196
+
197
+ lowered = content.lower()
198
+ score = 0.0
199
+ for keyword in keywords:
200
+ if keyword in lowered:
201
+ score += 1.0
202
+ if score <= 0:
203
+ continue
204
+
205
+ snippet = self._first_matching_snippet(content, keywords)
206
+ items.append(
207
+ KnowledgeItem(
208
+ source=self._format_source_path(file_path),
209
+ title=file_path.stem,
210
+ snippet=snippet,
211
+ score=score,
212
+ )
213
+ )
214
+
215
+ items.sort(key=lambda item: item.score, reverse=True)
216
+ return items[:max_results]
217
+
218
+ def _first_matching_snippet(self, content: str, keywords: list[str]) -> str:
219
+ lines = [line.strip() for line in content.splitlines() if line.strip()]
220
+ for line in lines:
221
+ lowered = line.lower()
222
+ if any(keyword in lowered for keyword in keywords):
223
+ return line[:220]
224
+ return (lines[0][:220] if lines else "")
225
+
226
+ def _format_source_path(self, file_path: Path) -> str:
227
+ try:
228
+ return str(file_path.relative_to(self.project_dir))
229
+ except ValueError:
230
+ try:
231
+ return f"builtin/{file_path.relative_to(self.builtin_data_dir)}"
232
+ except ValueError:
233
+ return str(file_path)
234
+
235
+ def _build_web_query(self, requirement: str, domain: str) -> str:
236
+ if domain:
237
+ return f"{requirement} {domain} best practices architecture ui ux"
238
+ return f"{requirement} best practices architecture ui ux"
239
+
240
+ def _collect_web_items(self, query: str, max_results: int) -> list[KnowledgeItem]:
241
+ if not self.web_enabled:
242
+ return []
243
+
244
+ results = self._collect_web_items_ddgs(query=query, max_results=max_results)
245
+ if results:
246
+ return results
247
+ return self._collect_web_items_duckduckgo(query=query, max_results=max_results)
248
+
249
+ def _collect_web_items_ddgs(self, query: str, max_results: int) -> list[KnowledgeItem]:
250
+ try:
251
+ from ddgs import DDGS # type: ignore
252
+ except Exception:
253
+ return []
254
+
255
+ results: list[KnowledgeItem] = []
256
+ try:
257
+ with DDGS() as ddgs:
258
+ entries = ddgs.text(query, max_results=max_results)
259
+ for index, entry in enumerate(entries):
260
+ if not isinstance(entry, dict):
261
+ continue
262
+ results.append(
263
+ KnowledgeItem(
264
+ source="web",
265
+ title=str(entry.get("title", "web-result")).strip(),
266
+ snippet=str(entry.get("body", "")).strip()[:220],
267
+ link=str(entry.get("href", "")).strip(),
268
+ score=float(max_results - index),
269
+ )
270
+ )
271
+ except Exception:
272
+ return []
273
+
274
+ return results
275
+
276
+ def _collect_web_items_duckduckgo(self, query: str, max_results: int) -> list[KnowledgeItem]:
277
+ encoded_query = urllib.parse.quote(query)
278
+ url = (
279
+ "https://api.duckduckgo.com/"
280
+ f"?q={encoded_query}&format=json&no_html=1&skip_disambig=1"
281
+ )
282
+
283
+ try:
284
+ response = requests.get(url, timeout=6)
285
+ if response.status_code >= 400:
286
+ return []
287
+ payload = response.text
288
+ data = json.loads(payload)
289
+ except Exception:
290
+ return []
291
+
292
+ results: list[KnowledgeItem] = []
293
+ abstract = str(data.get("Abstract", "")).strip()
294
+ if abstract:
295
+ results.append(
296
+ KnowledgeItem(
297
+ source="web",
298
+ title=str(data.get("Heading", "DuckDuckGo Result")).strip() or "DuckDuckGo Result",
299
+ snippet=abstract[:220],
300
+ link=str(data.get("AbstractURL", "")).strip(),
301
+ score=float(max_results),
302
+ )
303
+ )
304
+
305
+ related = data.get("RelatedTopics", [])
306
+ for item in related:
307
+ if len(results) >= max_results:
308
+ break
309
+ if not isinstance(item, dict):
310
+ continue
311
+ if "Topics" in item and isinstance(item.get("Topics"), list):
312
+ sub_topics = item.get("Topics", [])
313
+ else:
314
+ sub_topics = [item]
315
+
316
+ for topic in sub_topics:
317
+ if len(results) >= max_results:
318
+ break
319
+ if not isinstance(topic, dict):
320
+ continue
321
+ text = str(topic.get("Text", "")).strip()
322
+ if not text:
323
+ continue
324
+ results.append(
325
+ KnowledgeItem(
326
+ source="web",
327
+ title=text[:80],
328
+ snippet=text[:220],
329
+ link=str(topic.get("FirstURL", "")).strip(),
330
+ score=float(max_results - len(results)),
331
+ )
332
+ )
333
+
334
+ return results[:max_results]
335
+
336
+ def _compose_enriched_requirement(
337
+ self,
338
+ requirement: str,
339
+ local_items: list[KnowledgeItem],
340
+ web_items: list[KnowledgeItem],
341
+ ) -> str:
342
+ notes: list[str] = []
343
+ for item in local_items[:3]:
344
+ notes.append(f"本地知识参考: {item.title} - {item.snippet}")
345
+ for item in web_items[:3]:
346
+ notes.append(f"外部最佳实践: {item.title} - {item.snippet}")
347
+
348
+ if not notes:
349
+ return requirement
350
+
351
+ joined = ";".join(notes)
352
+ return f"{requirement}。请结合以下上下文实现:{joined}"
@@ -0,0 +1,356 @@
1
+ """
2
+ 质量评分引擎 - 基于文档内容和检查项计算真实质量分数
3
+
4
+ 开发:Excellent(11964948@qq.com)
5
+ 功能:为 WorkflowEngine 提供真实的质量评分算法
6
+ 作用:替代原来硬编码的 return 85.0,基于多维度评估
7
+ 创建时间:2026-01-29
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+
15
+
16
+ @dataclass
17
+ class QualityDimension:
18
+ """质量评估维度"""
19
+ name: str
20
+ score: int # 0-100
21
+ weight: float # 权重
22
+ details: str = ""
23
+
24
+
25
+ @dataclass
26
+ class PhaseQualityReport:
27
+ """阶段质量评估报告"""
28
+ phase_name: str
29
+ dimensions: list[QualityDimension] = field(default_factory=list)
30
+
31
+ @property
32
+ def weighted_score(self) -> float:
33
+ if not self.dimensions:
34
+ return 0.0
35
+ total_w = sum(d.weight for d in self.dimensions)
36
+ if total_w == 0:
37
+ return 0.0
38
+ return sum(d.score * d.weight for d in self.dimensions) / total_w
39
+
40
+ @property
41
+ def total_score(self) -> int:
42
+ return int(self.weighted_score)
43
+
44
+
45
+ class QualityScorer:
46
+ """
47
+ 质量评分引擎
48
+
49
+ 为 orchestrator/engine.py 中的每个阶段提供真实质量评分,
50
+ 替代硬编码的 return 85.0。
51
+ """
52
+
53
+ def __init__(self, project_dir: Path, name: str):
54
+ self.project_dir = Path(project_dir).resolve()
55
+ self.name = name
56
+ self.output_dir = self.project_dir / "output"
57
+
58
+ # ------------------------------------------------------------------
59
+ # 阶段评分入口
60
+ # ------------------------------------------------------------------
61
+
62
+ def score_discovery(self, context_data: dict) -> int:
63
+ """第 0 阶段:需求增强质量评分"""
64
+ report = PhaseQualityReport(phase_name="discovery")
65
+
66
+ # 1. 需求描述长度和完整度
67
+ description = context_data.get("description", "")
68
+ desc_score = min(100, max(40, len(description) // 2))
69
+ report.dimensions.append(QualityDimension(
70
+ name="需求描述完整度",
71
+ score=desc_score,
72
+ weight=1.5,
73
+ details=f"需求描述长度:{len(description)} 字符",
74
+ ))
75
+
76
+ # 2. 是否有知识库增强
77
+ has_kb_enhancement = bool(context_data.get("knowledge_enhanced"))
78
+ report.dimensions.append(QualityDimension(
79
+ name="知识库增强",
80
+ score=90 if has_kb_enhancement else 60,
81
+ weight=1.0,
82
+ details="知识库已注入" if has_kb_enhancement else "未进行知识库增强",
83
+ ))
84
+
85
+ # 3. 是否有联网检索
86
+ has_web_research = bool(context_data.get("web_research"))
87
+ report.dimensions.append(QualityDimension(
88
+ name="联网检索",
89
+ score=90 if has_web_research else 65,
90
+ weight=0.8,
91
+ details="已完成联网检索" if has_web_research else "未进行联网检索(离线模式)",
92
+ ))
93
+
94
+ return report.total_score
95
+
96
+ def score_documentation(self) -> int:
97
+ """第 1 阶段:文档生成质量评分"""
98
+ report = PhaseQualityReport(phase_name="documentation")
99
+
100
+ # PRD 文档
101
+ prd_score = self._score_doc_file(
102
+ f"{self.name}-prd.md",
103
+ required_sections=["产品愿景", "功能需求", "用户故事", "验收标准"],
104
+ min_length=2000,
105
+ )
106
+ report.dimensions.append(QualityDimension(
107
+ name="PRD 文档",
108
+ score=prd_score,
109
+ weight=1.5,
110
+ ))
111
+
112
+ # 架构文档
113
+ arch_score = self._score_doc_file(
114
+ f"{self.name}-architecture.md",
115
+ required_sections=["技术栈", "数据库", "API", "安全"],
116
+ min_length=2000,
117
+ )
118
+ report.dimensions.append(QualityDimension(
119
+ name="架构文档",
120
+ score=arch_score,
121
+ weight=1.5,
122
+ ))
123
+
124
+ # UI/UX 文档
125
+ uiux_score = self._score_doc_file(
126
+ f"{self.name}-uiux.md",
127
+ required_sections=["设计系统", "色彩"],
128
+ min_length=1000,
129
+ )
130
+ report.dimensions.append(QualityDimension(
131
+ name="UI/UX 文档",
132
+ score=uiux_score,
133
+ weight=1.0,
134
+ ))
135
+
136
+ return report.total_score
137
+
138
+ def score_frontend_scaffold(self) -> int:
139
+ """第 2 阶段:前端骨架质量评分"""
140
+ report = PhaseQualityReport(phase_name="frontend_scaffold")
141
+
142
+ # 前端蓝图文档
143
+ blueprint_score = self._score_doc_file(
144
+ f"{self.name}-frontend-blueprint.md",
145
+ required_sections=["信息架构", "页面", "组件"],
146
+ min_length=500,
147
+ )
148
+ report.dimensions.append(QualityDimension(
149
+ name="前端蓝图",
150
+ score=blueprint_score,
151
+ weight=1.2,
152
+ ))
153
+
154
+ # 执行计划
155
+ plan_score = self._score_doc_file(
156
+ f"{self.name}-execution-plan.md",
157
+ required_sections=["phase", "阶段"],
158
+ min_length=500,
159
+ )
160
+ report.dimensions.append(QualityDimension(
161
+ name="执行计划",
162
+ score=plan_score,
163
+ weight=1.0,
164
+ ))
165
+
166
+ return report.total_score
167
+
168
+ def score_spec(self) -> int:
169
+ """第 3 阶段:Spec 规范质量评分"""
170
+ spec_dir = self.project_dir / ".super-dev" / "changes"
171
+ if not spec_dir.exists():
172
+ return 55 # 没有 Spec,宽松评分
173
+
174
+ changes = [d for d in spec_dir.iterdir() if d.is_dir()]
175
+ if not changes:
176
+ return 55
177
+
178
+ # 检查最新的 change
179
+ latest = sorted(changes)[-1]
180
+ tasks_file = latest / "tasks.md"
181
+ proposal_file = latest / "proposal.md"
182
+
183
+ score = 50
184
+ if proposal_file.exists():
185
+ score += 20
186
+ if tasks_file.exists():
187
+ content = tasks_file.read_text(encoding="utf-8", errors="ignore")
188
+ # 计算任务数量
189
+ task_count = content.count("- [ ]") + content.count("- [x]")
190
+ score += min(30, task_count * 5)
191
+
192
+ return min(100, score)
193
+
194
+ def score_scaffold(self) -> int:
195
+ """第 4 阶段:前后端实现骨架质量评分"""
196
+ report = PhaseQualityReport(phase_name="scaffold")
197
+
198
+ impl_score = self._score_doc_file(
199
+ f"{self.name}-implementation.md",
200
+ required_sections=["目录结构", "API", "frontend", "backend"],
201
+ min_length=500,
202
+ )
203
+ report.dimensions.append(QualityDimension(
204
+ name="实现骨架",
205
+ score=impl_score,
206
+ weight=1.0,
207
+ ))
208
+
209
+ return report.total_score
210
+
211
+ def score_redteam(self) -> int:
212
+ """第 5 阶段:红队审查质量评分"""
213
+ redteam_score = self._score_doc_file(
214
+ f"{self.name}-redteam.md",
215
+ required_sections=["安全审查", "性能审查", "架构审查"],
216
+ min_length=500,
217
+ )
218
+
219
+ # 从报告中提取实际红队分数
220
+ redteam_file = self.output_dir / f"{self.name}-redteam.md"
221
+ if redteam_file.exists():
222
+ content = redteam_file.read_text(encoding="utf-8", errors="ignore")
223
+ # 尝试提取 "总分: XX/100" 格式
224
+ import re
225
+ match = re.search(r"总分[::]\s*(\d+)/100", content)
226
+ if match:
227
+ return int(match.group(1))
228
+
229
+ return redteam_score
230
+
231
+ def score_quality_gate(self) -> int:
232
+ """第 6 阶段:质量门禁报告评分"""
233
+ qg_file = self.output_dir / f"{self.name}-quality-gate.md"
234
+ if not qg_file.exists():
235
+ return 60
236
+
237
+ content = qg_file.read_text(encoding="utf-8", errors="ignore")
238
+
239
+ # 提取总分
240
+ import re
241
+ match = re.search(r"总分[::]\s*(\d+)/100", content)
242
+ if match:
243
+ return int(match.group(1))
244
+
245
+ # 检查是否通过
246
+ if "通过" in content and "未通过" not in content:
247
+ return 85
248
+ return 65
249
+
250
+ def score_code_review(self) -> int:
251
+ """第 7 阶段:代码审查指南评分"""
252
+ score = self._score_doc_file(
253
+ f"{self.name}-code-review.md",
254
+ required_sections=["审查清单", "安全", "性能"],
255
+ min_length=500,
256
+ )
257
+ return score
258
+
259
+ def score_ai_prompt(self) -> int:
260
+ """第 8 阶段:AI 提示词生成评分"""
261
+ score = self._score_doc_file(
262
+ f"{self.name}-ai-prompt.md",
263
+ required_sections=["任务列表", "开发规范", "文件结构"],
264
+ min_length=500,
265
+ )
266
+ return score
267
+
268
+ def score_cicd(self) -> int:
269
+ """第 9 阶段:CI/CD 配置评分"""
270
+ # 检查 .github/workflows 或类似目录
271
+ github_actions = self.project_dir / ".github" / "workflows"
272
+ if github_actions.exists() and any(github_actions.iterdir()):
273
+ return 90
274
+
275
+ # 检查 output 中的 CI/CD 文档
276
+ cicd_file = self.output_dir / f"{self.name}-cicd.md"
277
+ if cicd_file.exists():
278
+ content = cicd_file.read_text(encoding="utf-8", errors="ignore")
279
+ if len(content) > 500:
280
+ return 85
281
+ return 70
282
+
283
+ def score_migration(self) -> int:
284
+ """第 10 阶段:数据库迁移评分"""
285
+ migration_file = self.output_dir / f"{self.name}-migration.md"
286
+ if migration_file.exists():
287
+ content = migration_file.read_text(encoding="utf-8", errors="ignore")
288
+ if len(content) > 500:
289
+ return 88
290
+ return 65
291
+
292
+ # ------------------------------------------------------------------
293
+ # 通用阶段评分(fallback)
294
+ # ------------------------------------------------------------------
295
+
296
+ def score_phase(self, phase_name: str, context_data: dict | None = None) -> int:
297
+ """根据阶段名称路由到对应评分方法"""
298
+ ctx = context_data or {}
299
+ scorers = {
300
+ "discovery": lambda: self.score_discovery(ctx),
301
+ "intelligence": lambda: self.score_discovery(ctx), # 同 discovery 逻辑
302
+ "drafting": self.score_documentation,
303
+ "frontend": self.score_frontend_scaffold,
304
+ "spec": self.score_spec,
305
+ "scaffold": self.score_scaffold,
306
+ "redteam": self.score_redteam,
307
+ "qa": self.score_quality_gate,
308
+ "code_review": self.score_code_review,
309
+ "ai_prompt": self.score_ai_prompt,
310
+ "deployment": self.score_cicd,
311
+ "migration": self.score_migration,
312
+ "delivery": self.score_ai_prompt,
313
+ }
314
+ fn = scorers.get(phase_name.lower())
315
+ if fn:
316
+ try:
317
+ return fn()
318
+ except Exception:
319
+ return 75 # 评分失败时给默认分
320
+ return 80 # 未知阶段默认分
321
+
322
+ # ------------------------------------------------------------------
323
+ # 内部辅助
324
+ # ------------------------------------------------------------------
325
+
326
+ def _score_doc_file(
327
+ self,
328
+ filename: str,
329
+ required_sections: list[str],
330
+ min_length: int = 500,
331
+ ) -> int:
332
+ """评估 output/ 目录下指定文档文件的质量"""
333
+ doc_path = self.output_dir / filename
334
+ if not doc_path.exists():
335
+ return 40 # 文件不存在
336
+
337
+ content = doc_path.read_text(encoding="utf-8", errors="ignore")
338
+ if len(content) < 100:
339
+ return 45 # 内容太少
340
+
341
+ # 基础分:文件存在且有内容
342
+ base = 60
343
+
344
+ # 长度加分
345
+ if len(content) >= min_length:
346
+ base += 10
347
+ if len(content) >= min_length * 2:
348
+ base += 5
349
+
350
+ # 关键章节检查
351
+ per_section = 25 // max(len(required_sections), 1)
352
+ for section in required_sections:
353
+ if section.lower() in content.lower():
354
+ base += per_section
355
+
356
+ return min(100, base)