250 lines
8.3 KiB
Python
250 lines
8.3 KiB
Python
"""检查 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 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_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())
|