Files
dev_harness/scripts/check_harness.py
T

244 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""检查 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",
"scripts/wiki_docs.py",
"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_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}
for page, expected_path in CORE_PAGE_PATHS.items():
if configured_mappings.get(page) != expected_path:
errors.append(
f"核心 Wiki 页面映射缺失或路径错误:{page} -> {expected_path}"
)
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_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())