"""检查 DevHarness 必需文件、核心文档和任务归档的基本结构。""" from __future__ import annotations import argparse import json 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" ), "Existing-Project-Adoption-Guide": ( "docs/08-existing-project-adoption.md" ), "Delivery-Documentation-Guide": "docs/delivery/README.md", "Audience-Document-Template": ( "docs/delivery/audience-document-template.md" ), "Task-Archive-Template": "docs/templates/task-archive.md", } CORE_DOCUMENT_REQUIREMENTS = { "docs/README.md": ( "## 第一次阅读", "## 五分钟开始", "## 简单修改从哪里开始", "## 事实来源", ), "docs/00-project-profile.md": ( "## 基本信息", "## 子项目与交付单元", "## 技术栈与运行环境", "## 阅读入口", "## 常用命令", "## 环境、配置与凭据", "## Sense/Bell GoAdmin 固定技术基线", ), "docs/01-workflow.md": ( "## 面向初级维护者的修改边界", "## go-admin / go-admin-ui 开发约束", "## 每个任务的文档影响", "## 需求记录与流转", "## 稳定文档与任务归档", "## 自然语言快捷指令", "## 效率与范围控制", "### 严格控制范围", "### 渐进执行和修复", "### 复用已验证事实", "### 明确停止条件", ), "docs/02-architecture-and-code-map.md": ( "## 项目定位", "## 代码地图", "## 两条主要执行路径", "## 不可破坏的边界", ), "docs/03-business-rules-and-glossary.md": ( "## 核心术语", "## 工单状态", "## 稳定业务规则", "## 新项目需要补充什么", ), "docs/04-local-development-and-verification.md": ( "## 环境要求", "## Sense/Bell 固定工具链与只读参考源", "## 第一次运行", "## 常用调试方式", "## 完成修改前", ), "docs/05-common-changes.md": ( "## 风险分级", "## 修改 Wiki 文案", "## 调整 Harness 检查", "## 看懂 Agent 的修改", ), "docs/06-troubleshooting.md": ( "## 排查顺序", "## 必须停止的情况", ), "docs/07-new-project-documentation-setup.md": ( "## 初始化顺序", "### 2. 识别子项目与交付单元", "### 6. 确定交付对象和文档", "## 完成标准", ), "docs/08-existing-project-adoption.md": ( "## 与新项目初始化的区别", "## 接入前只读盘点", "## 已有内容保护原则", "## 多应用单仓库判断", "### 适合继续单仓库", "### 可以考虑拆仓", "### 保持单仓库时的最小规则", "## 增量接入顺序", "## 冲突处理和停止条件", "## 可复制 Agent 指令", "### 只分析", "### 方案确认后实施", "## 最小验收清单", "## 回退原则", ), "docs/delivery/README.md": ( "## 什么时候需要交付文档", "## 受众与文档选择", "## 内部文档与交付文档边界", "## 编写和维护流程", "## 最小验收清单", ), "docs/delivery/audience-document-template.md": ( "## 文档信息", "## 目的与适用范围", "## 前置条件", "## 操作步骤", "## 常见错误与恢复", "## 安全与权限", "## 已知限制", "## 支持与升级处理", "## 版本记录", "## 交付前检查", ), } REQUIRED_FILES = ( "AGENTS.md", "CLAUDE.md", "README.md", "docs/00-project-profile.md", "docs/01-workflow.md", "docs/templates/task-archive.md", *CORE_DOCUMENT_REQUIREMENTS, "wiki-docs.json", "goadmin-baseline.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 = ( "- 任务类型:单项目 / 协同", "- 主项目:Sense / Brain / Bell / contracts / 根级", "- 主 agent:", "## 依赖与并行", "- 前置工单:无 / #编号", "- 是否允许与前置工单并行:是 / 否", "- 原因:", "## 子项目影响", "- 仅影响的子项目 / 交付单元:", "- 是否跨子项目:是 / 否", "- 是否修改共享接口或契约:是 / 否;唯一事实来源:", "- write_paths:", "- 各子项目需要执行的验证:", "## 协同接口", "- 生产者:", "- 消费者:", "- 契约/共享事实源:", "- 兼容策略:不适用 / 向后兼容 / 发布新版本", "- 被阻塞或需要适配的工单:", "- 集成顺序:", "## 原始需求", "- 来源:用户对话 / Gitea / 其他", "- 提出时间:", "- 关键原话或脱敏摘要:", "## 需求变化记录", "| 日期 | 变化内容 | 原因 | 用户确认 |", "## 文档影响", "- [ ] 不影响长期文档,原因:", "- [ ] 更新架构与代码地图", "- [ ] 更新业务规则与术语", "- [ ] 更新常见修改或故障排查", "## 交付文档影响", "- [ ] 无交付文档影响,原因:", "- [ ] 更新已有交付文档,受众与页面:", "- [ ] 新增交付文档,受众与页面:", "- [ ] 需要目标岗位或客户代表验证:是 / 否;验证方式:", ) 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", "提交只包含当前工单相关文件", "### 自然语言快捷指令", "### 三项目并行建单顺序", "建单顺序不等于实施顺序", "主 agent / dispatcher", "`只分析`", "`建工单`", "`执行工单 #N`", "`建工单并做`", "`继续工单 #N`", "`检查工单 #N`", "`同步文档`", "`#N 验收通过`", "### 需求记录与流转", "不得臆造用户原话", "不复制完整聊天", "Gitea 工单全文不导出到仓库", ) for section in missing_sections(content, required): errors.append(f"AGENTS.md 缺少:{section}") def check_go_admin_ui_rules(errors: list[str], root: Path = ROOT) -> None: """检查 Sense、Bell 共用的框架精简和组件复用规则。""" requirements = { "AGENTS.md": ( "### go-admin / go-admin-ui 精简与复用", "最小可见、最小启用", "隐藏只移除用户入口,不等于禁用", "永久删除默认模块必须单独建立清理工单", "新页面优先复用现有布局", "不得引入与整体风格冲突的独立视觉体系", "跨产品影响按协同工单处理", "没有可见但不可用的失效入口", ), "Sense/AGENTS.md": ( "## go-admin / go-admin-ui 约束", "菜单权限或路由开关隐藏", "不能只隐藏菜单", "新增组件前在工单中说明", "同时影响 Bell 或共享基线时升级为协同工单", ), "Bell/AGENTS.md": ( "## go-admin / go-admin-ui 约束", "菜单权限或路由开关隐藏", "不能只隐藏菜单", "新增组件前在工单中说明", "同时影响 Sense 或共享基线时升级为协同工单", ), } for relative_path, required in requirements.items(): path = root / relative_path if not path.is_file(): errors.append(f"缺少 go-admin 规则文件:{relative_path}") continue content = path.read_text(encoding="utf-8") for section in missing_sections(content, required): errors.append(f"{relative_path} 缺少 go-admin 规则:{section}") GOADMIN_BASELINE_REQUIREMENTS = { "runtimes": { "go": "1.26.5", "node": "22.22.1", "package_manager": "pnpm", "pnpm": "9.15.1", }, "sources": { "go-admin": { "commit": "f06540883b41d03782bb6b2c4150f298f328c6b6", "upstream_url": "https://github.com/go-admin-team/go-admin.git", "branch": "master", "describe": "v2.2.0-25-gf065408", "exact_tag": None, "local_reference_path": r"D:\github\goadmin\go-admin", "license": "MIT", }, "go-admin-ui": { "commit": "67d393d713877572fab0b897296a4c1d525fc81d", "upstream_url": "https://github.com/go-admin-team/go-admin-ui.git", "branch": "master", "describe": "v2.0.9-170-g67d393d", "exact_tag": None, "local_reference_path": r"D:\github\goadmin\go-admin-ui", "package_version": "2.0.9", "package_version_is_authoritative": False, "license": "MIT", }, "go-admin-doc": { "commit": "424855aacf6905f3fde860c3331385cb25529a0d", "upstream_url": "https://github.com/go-admin-team/go-admin-doc.git", "branch": "master", "describe": "424855a", "exact_tag": None, "local_reference_path": r"D:\github\goadmin\go-admin-doc", "package_version_is_authoritative": False, "license": "MIT", }, }, } def check_goadmin_baseline(errors: list[str], root: Path = ROOT) -> None: """检查 Sense/Bell 共用上游和工具链基线可复现且规则完整。""" path = root / "goadmin-baseline.json" if not path.is_file(): errors.append("缺少 GoAdmin 技术基线:goadmin-baseline.json") return try: baseline = json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: errors.append(f"GoAdmin 技术基线不是有效 JSON:{exc}") return for group, entries in GOADMIN_BASELINE_REQUIREMENTS.items(): actual_group = baseline.get(group) if not isinstance(actual_group, dict): errors.append(f"GoAdmin 技术基线缺少对象:{group}") continue for name, expected in entries.items(): actual = actual_group.get(name) if isinstance(expected, dict): if not isinstance(actual, dict): errors.append(f"GoAdmin 技术基线缺少来源:{name}") continue for field, value in expected.items(): if actual.get(field) != value: errors.append( f"GoAdmin 技术基线不匹配:{name}.{field} 应为 {value}" ) elif actual != expected: errors.append( f"GoAdmin 技术基线不匹配:{group}.{name} 应为 {expected}" ) if baseline.get("authority") != "upstream_url_and_commit": errors.append("GoAdmin 技术基线必须以上游 URL 和 commit 为权威标识") rules = baseline.get("rules") required_rules = ( "local_repositories_are_read_only", "product_code_must_not_be_developed_in_reference_repositories", "preserve_upstream_license_and_copyright", "upgrades_require_coordination_issue", "sense_and_bell_release_independently", ) if not isinstance(rules, dict): errors.append("GoAdmin 技术基线缺少 rules") else: for rule in required_rules: if rules.get(rule) is not True: errors.append(f"GoAdmin 技术基线规则必须为 true:{rule}") agent_requirements = { "AGENTS.md": ( "### Sense/Bell GoAdmin 技术基线", "`goadmin-baseline.json`", "上游 URL 加完整 commit 是可复现标识", "当前机器的只读参考副本", "必须建立 Sense/Bell 协同升级工单", "本机 Go 1.23.0 不满足要求", ), "Sense/AGENTS.md": ( "## GoAdmin 技术基线", "读取根 `goadmin-baseline.json`", "不得在参考仓库原地开发", "不得单独漂移上游或工具链版本", ), "Bell/AGENTS.md": ( "## GoAdmin 技术基线", "读取根 `goadmin-baseline.json`", "不得在参考仓库原地开发", "不得单独漂移上游或工具链版本", ), } for relative_path, required in agent_requirements.items(): agent_path = root / relative_path if not agent_path.is_file(): errors.append(f"缺少 GoAdmin 基线规则文件:{relative_path}") continue content = agent_path.read_text(encoding="utf-8") for section in missing_sections(content, required): errors.append(f"{relative_path} 缺少 GoAdmin 基线规则:{section}") def check_claude_code_entry(errors: list[str], root: Path = ROOT) -> None: """检查 Claude Code 入口直接复用共同 Agent 规则。""" path = root / "CLAUDE.md" if not path.is_file(): return content = path.read_text(encoding="utf-8") lines = {line.strip() for line in content.splitlines()} if "@AGENTS.md" not in lines: errors.append("CLAUDE.md 缺少独立的 @AGENTS.md 导入") required = ( "共同规则事实来源", "只记录 Claude Code 特有", "只修改 `AGENTS.md`", "## 模型路由", "## Agent 交接", "## 三项目角色路由", "## Haiku 只读约束", "当前模型足以完成任务时不升级模型", "Opus 输出方案后必须等待用户确认", "不让 Haiku 决定最终根因", "只读必须通过子 Agent 工具权限实现", "主 Claude 作为 dispatcher", "coordination agent", ) for section in missing_sections(content, required): errors.append(f"CLAUDE.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_go_admin_ui_rules(errors) check_goadmin_baseline(errors) check_claude_code_entry(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())