#!/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 ") 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()