"""检查 DevHarness 必需文件、核心文档和任务归档的基本结构。""" from __future__ import annotations import argparse import re from pathlib import Path from wiki_docs import WikiDocsError, load_config, parse_mirror ROOT = Path(__file__).resolve().parents[1] CORE_PAGE_PATHS = { "Home": "docs/README.md", "Project-Profile": "docs/00-project-profile.md", "Development-Workflow": "docs/01-workflow.md", "Architecture-and-Code-Map": "docs/02-architecture-and-code-map.md", "Business-Rules-and-Glossary": "docs/03-business-rules-and-glossary.md", "Local-Development-and-Verification": ( "docs/04-local-development-and-verification.md" ), "Common-Changes": "docs/05-common-changes.md", "Troubleshooting": "docs/06-troubleshooting.md", "New-Project-Documentation-Setup": ( "docs/07-new-project-documentation-setup.md" ), "Task-Archive-Template": "docs/templates/task-archive.md", } CORE_DOCUMENT_REQUIREMENTS = { "docs/README.md": ( "## 第一次阅读", "## 五分钟开始", "## 简单修改从哪里开始", "## 事实来源", ), "docs/00-project-profile.md": ( "## 基本信息", "## 技术栈与运行环境", "## 阅读入口", "## 常用命令", "## 环境、配置与凭据", ), "docs/01-workflow.md": ( "## 面向初级维护者的修改边界", "## 每个任务的文档影响", "## 稳定文档与任务归档", "## 效率与范围控制", "### 严格控制范围", "### 渐进执行和修复", "### 复用已验证事实", "### 明确停止条件", ), "docs/02-architecture-and-code-map.md": ( "## 项目定位", "## 代码地图", "## 两条主要执行路径", "## 不可破坏的边界", ), "docs/03-business-rules-and-glossary.md": ( "## 核心术语", "## 工单状态", "## 稳定业务规则", "## 新项目需要补充什么", ), "docs/04-local-development-and-verification.md": ( "## 环境要求", "## 第一次运行", "## 常用调试方式", "## 完成修改前", ), "docs/05-common-changes.md": ( "## 风险分级", "## 修改 Wiki 文案", "## 调整 Harness 检查", "## 看懂 Agent 的修改", ), "docs/06-troubleshooting.md": ( "## 排查顺序", "## 必须停止的情况", ), "docs/07-new-project-documentation-setup.md": ( "## 初始化顺序", "## 完成标准", ), } REQUIRED_FILES = ( "AGENTS.md", "README.md", "docs/00-project-profile.md", "docs/01-workflow.md", "docs/templates/task-archive.md", *CORE_DOCUMENT_REQUIREMENTS, "wiki-docs.json", "dev_scripts/wiki_docs.py", "dev_scripts/sync_wiki_docs.py", ".gitea/issue_template/epic.md", ".gitea/issue_template/mvp.md", ".gitea/issue_template/task.md", ) ARCHIVE_HEADINGS = ( "## 背景与目标", "## 最终方案", "## 修改文件", "## 验收结果", "## 测试", "## 相关提交", ) def check_required_files(errors: list[str]) -> None: for relative_path in REQUIRED_FILES: if not (ROOT / relative_path).is_file(): errors.append(f"缺少必需文件:{relative_path}") def check_project_profile(errors: list[str], warnings: list[str], strict: bool) -> None: profile = ROOT / "docs" / "00-project-profile.md" if not profile.is_file(): return if "<填写" in profile.read_text(encoding="utf-8"): message = "项目档案仍有未填写内容" (errors if strict else warnings).append(message) def check_archives(errors: list[str]) -> None: task_dir = ROOT / "docs" / "task" for path in task_dir.glob("*.md"): if not re.match(r"^\d+-.+\.md$", path.name): errors.append(f"归档文件名不符合 <编号>-<标题>.md:{path.name}") content = path.read_text(encoding="utf-8") for heading in ARCHIVE_HEADINGS: if heading not in content: errors.append(f"{path.name} 缺少章节:{heading}") if "**未验证部分**:" not in content: errors.append(f"{path.name} 没有记录未验证部分") def missing_sections(content: str, required: tuple[str, ...]) -> list[str]: return [section for section in required if section not in content] def check_core_documents(errors: list[str], root: Path = ROOT) -> None: """检查初级维护者所需主题页的固定结构。""" for relative_path, required in CORE_DOCUMENT_REQUIREMENTS.items(): path = root / relative_path if not path.is_file(): continue try: _, body = parse_mirror(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, WikiDocsError): continue for section in missing_sections(body, required): errors.append(f"{relative_path} 缺少核心章节:{section}") def check_task_template(errors: list[str], root: Path = ROOT) -> None: path = root / ".gitea" / "issue_template" / "task.md" if not path.is_file(): return content = path.read_text(encoding="utf-8") required = ( "## 文档影响", "- [ ] 不影响长期文档,原因:", "- [ ] 更新架构与代码地图", "- [ ] 更新业务规则与术语", "- [ ] 更新常见修改或故障排查", ) for section in missing_sections(content, required): errors.append(f"单元任务模板缺少:{section}") def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None: path = root / "AGENTS.md" if not path.is_file(): return content = path.read_text(encoding="utf-8") required = ( "### 效率与范围控制", "#### 严格控制范围", "#### 渐进执行和修复", "#### 复用已验证事实", "#### 明确停止条件", "单元任务是唯一正式实施单位", "高风险修改必须停止", "用户没有明确验收通过前不得关闭", "长期文档必须先修改 Wiki", "提交只包含当前工单相关文件", ) for section in missing_sections(content, required): errors.append(f"AGENTS.md 缺少:{section}") def core_mapping_errors(configured_mappings: dict[str, str]) -> list[str]: errors: list[str] = [] for page, expected_path in CORE_PAGE_PATHS.items(): if configured_mappings.get(page) != expected_path: errors.append( f"核心 Wiki 页面映射缺失或路径错误:{page} -> {expected_path}" ) return errors def check_wiki_mirrors(errors: list[str]) -> None: """检查每份本地文档都有显式映射和可追踪的镜像头。""" try: config = load_config() except WikiDocsError as exc: errors.append(str(exc)) return configured_mappings = {mapping.page: mapping.path for mapping in config.mappings} errors.extend(core_mapping_errors(configured_mappings)) mapped_paths = {mapping.path for mapping in config.mappings} actual_paths = { path.relative_to(ROOT).as_posix() for path in (ROOT / "docs").rglob("*.md") } for path in sorted(actual_paths - mapped_paths): errors.append(f"docs 中存在未登记的 Wiki 镜像:{path}") for mapping in config.mappings: path = ROOT / mapping.path if not path.is_file(): errors.append(f"缺少 Wiki 镜像:{mapping.path}") continue try: metadata, _ = parse_mirror(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, WikiDocsError) as exc: errors.append(f"Wiki 镜像无效 {mapping.path}:{exc}") continue if metadata.get("generated") != "true (请先修改 Gitea Wiki,禁止直接编辑本文件)": errors.append(f"{mapping.path} 没有只读镜像标记") if metadata.get("wiki_page") != mapping.page: errors.append(f"{mapping.path} 的 wiki_page 与映射不一致") revision = metadata.get("wiki_revision", "") if re.fullmatch(r"[0-9a-f]{40,64}", revision) is None: errors.append(f"{mapping.path} 的 wiki_revision 无效") if not metadata.get("synchronized_at"): errors.append(f"{mapping.path} 缺少 synchronized_at") def main() -> int: parser = argparse.ArgumentParser(description="检查 DevHarness 项目结构") parser.add_argument( "--strict", action="store_true", help="项目档案有占位内容时返回失败", ) args = parser.parse_args() errors: list[str] = [] warnings: list[str] = [] check_required_files(errors) check_project_profile(errors, warnings, args.strict) check_wiki_mirrors(errors) check_core_documents(errors) check_task_template(errors) check_agent_efficiency_rules(errors) check_archives(errors) for warning in warnings: print(f"警告:{warning}") for error in errors: print(f"错误:{error}") if errors: print(f"检查失败:{len(errors)} 个问题") return 1 print("DevHarness 检查通过") return 0 if __name__ == "__main__": raise SystemExit(main())