247 lines
8.4 KiB
Python
247 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
docmap 输出质量验证脚本
|
||
检查生成的文档是否符合规范要求
|
||
|
||
检查项分两级:
|
||
- 失败项(failed):结构/内容缺失,退出码非零
|
||
- 警告项(warning):可疑但不阻断(如 stateDiagram、模糊表述),不影响退出码
|
||
"""
|
||
|
||
import re
|
||
import sys
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from dataclasses import dataclass
|
||
from typing import List
|
||
|
||
|
||
# 模糊表述"等"的白名单(合理用法,不算模糊)
|
||
DENG_WHITELIST = [
|
||
"等于", "等待", "对等", "等级", "等同", "同等", "均等",
|
||
"不等", "稍等", "优等", "劣等", "等比", "等值",
|
||
]
|
||
|
||
|
||
@dataclass
|
||
class ValidationResult:
|
||
passed: bool
|
||
message: str
|
||
file: str = ""
|
||
warning: bool = False # True 表示警告项,不计入失败、不影响退出码
|
||
|
||
|
||
class DocmapValidator:
|
||
def __init__(self, outputs_dir: str):
|
||
self.outputs_dir = Path(outputs_dir)
|
||
self.results: List[ValidationResult] = []
|
||
|
||
def validate(self) -> List[ValidationResult]:
|
||
"""执行所有验证"""
|
||
self.results = []
|
||
|
||
# 检查目录结构
|
||
self._check_directory_structure()
|
||
|
||
# 验证架构文档
|
||
self._validate_architecture_doc()
|
||
|
||
# 验证模块文档
|
||
self._validate_module_docs()
|
||
|
||
# 验证能力模型
|
||
self._validate_capability_doc()
|
||
|
||
return self.results
|
||
|
||
def _check_directory_structure(self):
|
||
"""检查输出目录结构"""
|
||
if not self.outputs_dir.exists():
|
||
self.results.append(ValidationResult(
|
||
False, "输出目录不存在", str(self.outputs_dir)
|
||
))
|
||
return
|
||
|
||
# 检查必需文件
|
||
required_files = ["01-产品整体架构.md", "03-系统能力模型.md"]
|
||
for f in required_files:
|
||
path = self.outputs_dir / f
|
||
if not path.exists():
|
||
self.results.append(ValidationResult(
|
||
False, f"缺少必需文件: {f}", str(self.outputs_dir)
|
||
))
|
||
|
||
# 检查模块目录
|
||
module_dir = self.outputs_dir / "02-功能模块"
|
||
if not module_dir.exists():
|
||
self.results.append(ValidationResult(
|
||
False, "缺少功能模块目录", str(self.outputs_dir)
|
||
))
|
||
|
||
def _validate_architecture_doc(self):
|
||
"""验证架构文档"""
|
||
doc_path = self.outputs_dir / "01-产品整体架构.md"
|
||
if not doc_path.exists():
|
||
return
|
||
|
||
content = doc_path.read_text(encoding='utf-8')
|
||
|
||
# 检查 7 个必需章节
|
||
required_sections = [
|
||
"产品定位",
|
||
"系统整体架构",
|
||
"业务架构图",
|
||
"核心业务流程",
|
||
"数据架构",
|
||
"权限与角色体系",
|
||
"系统扩展点分析"
|
||
]
|
||
|
||
for section in required_sections:
|
||
if section not in content:
|
||
self.results.append(ValidationResult(
|
||
False, f"缺少章节: {section}", str(doc_path)
|
||
))
|
||
|
||
# 检查 Mermaid 图(至少 2 个)
|
||
mermaid_count = content.count("```mermaid")
|
||
if mermaid_count < 2:
|
||
self.results.append(ValidationResult(
|
||
False, f"Mermaid 图数量不足: 需要至少 2 个,实际 {mermaid_count} 个", str(doc_path)
|
||
))
|
||
else:
|
||
self.results.append(ValidationResult(
|
||
True, f"Mermaid 图数量: {mermaid_count} 个", str(doc_path)
|
||
))
|
||
|
||
# 检查模糊表述(警告项,不阻断)
|
||
fuzzy_patterns = [r"等情况", r"等多种", r"其他相关", r"等等", r"诸如此类"]
|
||
for pattern in fuzzy_patterns:
|
||
matches = re.findall(pattern, content)
|
||
if matches:
|
||
self.results.append(ValidationResult(
|
||
False, f"发现模糊表述 '{pattern}': 出现 {len(matches)} 次",
|
||
str(doc_path), warning=True
|
||
))
|
||
|
||
def _validate_module_docs(self):
|
||
"""验证模块文档"""
|
||
module_dir = self.outputs_dir / "02-功能模块"
|
||
if not module_dir.exists():
|
||
return
|
||
|
||
module_files = list(module_dir.glob("*.md"))
|
||
if not module_files:
|
||
self.results.append(ValidationResult(
|
||
False, "功能模块目录为空", str(module_dir)
|
||
))
|
||
return
|
||
|
||
for module_file in module_files:
|
||
content = module_file.read_text(encoding='utf-8')
|
||
|
||
# 检查功能清单表格
|
||
if "| 功能名称 |" not in content and "|功能名称|" not in content:
|
||
self.results.append(ValidationResult(
|
||
False, "缺少功能清单表格", str(module_file)
|
||
))
|
||
|
||
# 检查状态流转 Mermaid(警告项,不阻断:无状态机的模块可豁免)
|
||
if "```mermaid" not in content or "stateDiagram" not in content:
|
||
self.results.append(ValidationResult(
|
||
False, "缺少状态流转 Mermaid 图", str(module_file), warning=True
|
||
))
|
||
|
||
# 检查模糊表述"等"(警告项,不阻断;白名单词不算命中)
|
||
hit_lines = []
|
||
for lineno, line in enumerate(content.split('\n'), 1):
|
||
stripped = line
|
||
for word in DENG_WHITELIST:
|
||
stripped = stripped.replace(word, "")
|
||
if "等" in stripped:
|
||
hit_lines.append(lineno)
|
||
if hit_lines:
|
||
self.results.append(ValidationResult(
|
||
False,
|
||
f"可能包含模糊表述 '等'(行: {', '.join(map(str, hit_lines))})",
|
||
str(module_file), warning=True
|
||
))
|
||
|
||
def _validate_capability_doc(self):
|
||
"""验证能力模型文档"""
|
||
doc_path = self.outputs_dir / "03-系统能力模型.md"
|
||
if not doc_path.exists():
|
||
return
|
||
|
||
content = doc_path.read_text(encoding='utf-8')
|
||
|
||
# 检查能力依赖关系图
|
||
if "```mermaid" not in content:
|
||
self.results.append(ValidationResult(
|
||
False, "缺少能力依赖关系图", str(doc_path)
|
||
))
|
||
|
||
# 检查平台级/业务定制能力区分
|
||
if "平台级能力" not in content or "业务定制能力" not in content:
|
||
self.results.append(ValidationResult(
|
||
False, "缺少平台级/业务定制能力区分", str(doc_path)
|
||
))
|
||
|
||
def generate_report(self) -> str:
|
||
"""生成验证报告"""
|
||
passed = [r for r in self.results if r.passed]
|
||
warnings = [r for r in self.results if not r.passed and r.warning]
|
||
failed = [r for r in self.results if not r.passed and not r.warning]
|
||
|
||
report = ["# docmap 输出质量验证报告\n"]
|
||
report.append(f"**验证时间:** {datetime.now().isoformat()}\n")
|
||
report.append(f"**输出目录:** {self.outputs_dir}\n")
|
||
report.append(f"**通过项:** {len(passed)}\n")
|
||
report.append(f"**警告项:** {len(warnings)}\n")
|
||
report.append(f"**失败项:** {len(failed)}\n\n")
|
||
|
||
if failed:
|
||
report.append("## ❌ 失败项\n")
|
||
for r in failed:
|
||
report.append(f"- **{r.file}**: {r.message}\n")
|
||
report.append("\n")
|
||
|
||
if warnings:
|
||
report.append("## ⚠️ 警告项(不阻断)\n")
|
||
for r in warnings:
|
||
report.append(f"- **{r.file}**: {r.message}\n")
|
||
report.append("\n")
|
||
|
||
if passed:
|
||
report.append("## ✅ 通过项\n")
|
||
for r in passed:
|
||
report.append(f"- **{r.file}**: {r.message}\n")
|
||
|
||
return "\n".join(report)
|
||
|
||
|
||
def main():
|
||
if len(sys.argv) < 2:
|
||
print("Usage: python validate_output.py <outputs_directory>")
|
||
sys.exit(1)
|
||
|
||
outputs_dir = sys.argv[1]
|
||
validator = DocmapValidator(outputs_dir)
|
||
validator.validate()
|
||
|
||
report = validator.generate_report()
|
||
print(report)
|
||
|
||
# 保存报告
|
||
report_path = Path(outputs_dir).parent / "validation_report.md"
|
||
report_path.write_text(report, encoding='utf-8')
|
||
print(f"\n报告已保存: {report_path}")
|
||
|
||
# 仅失败项影响退出码;警告项不阻断
|
||
failed = [r for r in validator.results if not r.passed and not r.warning]
|
||
sys.exit(1 if failed else 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|