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.
- super_dev/__init__.py +11 -0
- super_dev/analyzer/__init__.py +34 -0
- super_dev/analyzer/analyzer.py +440 -0
- super_dev/analyzer/detectors.py +511 -0
- super_dev/analyzer/models.py +285 -0
- super_dev/cli.py +3257 -0
- super_dev/config/__init__.py +11 -0
- super_dev/config/frontend.py +557 -0
- super_dev/config/manager.py +281 -0
- super_dev/creators/__init__.py +26 -0
- super_dev/creators/creator.py +134 -0
- super_dev/creators/document_generator.py +2473 -0
- super_dev/creators/frontend_builder.py +371 -0
- super_dev/creators/implementation_builder.py +789 -0
- super_dev/creators/prompt_generator.py +289 -0
- super_dev/creators/requirement_parser.py +354 -0
- super_dev/creators/spec_builder.py +195 -0
- super_dev/deployers/__init__.py +20 -0
- super_dev/deployers/cicd.py +1269 -0
- super_dev/deployers/delivery.py +229 -0
- super_dev/deployers/migration.py +1032 -0
- super_dev/design/__init__.py +74 -0
- super_dev/design/aesthetics.py +530 -0
- super_dev/design/charts.py +396 -0
- super_dev/design/codegen.py +379 -0
- super_dev/design/engine.py +528 -0
- super_dev/design/generator.py +395 -0
- super_dev/design/landing.py +422 -0
- super_dev/design/tech_stack.py +524 -0
- super_dev/design/tokens.py +269 -0
- super_dev/design/ux_guide.py +391 -0
- super_dev/exceptions.py +119 -0
- super_dev/experts/__init__.py +19 -0
- super_dev/experts/service.py +161 -0
- super_dev/integrations/__init__.py +7 -0
- super_dev/integrations/manager.py +264 -0
- super_dev/orchestrator/__init__.py +12 -0
- super_dev/orchestrator/engine.py +958 -0
- super_dev/orchestrator/experts.py +423 -0
- super_dev/orchestrator/knowledge.py +352 -0
- super_dev/orchestrator/quality.py +356 -0
- super_dev/reviewers/__init__.py +17 -0
- super_dev/reviewers/code_review.py +471 -0
- super_dev/reviewers/quality_gate.py +964 -0
- super_dev/reviewers/redteam.py +881 -0
- super_dev/skills/__init__.py +7 -0
- super_dev/skills/manager.py +307 -0
- super_dev/specs/__init__.py +44 -0
- super_dev/specs/generator.py +264 -0
- super_dev/specs/manager.py +428 -0
- super_dev/specs/models.py +348 -0
- super_dev/specs/validator.py +415 -0
- super_dev/utils/__init__.py +11 -0
- super_dev/utils/logger.py +133 -0
- super_dev/web/api.py +1402 -0
- super_dev-2.0.0.dist-info/METADATA +252 -0
- super_dev-2.0.0.dist-info/RECORD +61 -0
- super_dev-2.0.0.dist-info/WHEEL +5 -0
- super_dev-2.0.0.dist-info/entry_points.txt +2 -0
- super_dev-2.0.0.dist-info/licenses/LICENSE +21 -0
- 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)
|