Files
aios/tools/aios_v2_docs_code_check.py
T

272 lines
9.7 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.
#!/usr/bin/env python3
"""AIOS V2 文档—代码匹配门禁。
仅使用 Python 标准库,可由本地、Gitea Actions、Codex 监督任务或发布验收脚本调用。
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
@dataclass(frozen=True)
class CheckResult:
level: str
message: str
REQUIRED_DOCUMENTS = (
"docs/aios-v2/README.md",
"docs/aios-v2/01-USER-REQUIREMENTS.md",
"docs/aios-v2/02-TARGET-ARCHITECTURE.md",
"docs/aios-v2/03-MIGRATION-AND-DELIVERY.md",
"docs/aios-v2/04-MODULE-CONTRACTS.md",
"docs/aios-v2/05-TRACEABILITY-MATRIX.md",
"docs/aios-v2/06-DOCUMENT-GOVERNANCE.md",
"docs/aios-v2/adr/README.md",
"docs/aios-v2/adr/ADR-001-single-control-plane.md",
"docs/aios-v2/adr/ADR-002-openclaw-hermes-decoupling.md",
"docs/aios-v2/adr/ADR-003-gitops-only-change-flow.md",
"docs/aios-v2/adr/ADR-004-multi-tenant-isolation.md",
"docs/aios-v2/adr/ADR-005-document-code-sync-gate.md",
"docs/aios-v2/adr/ADR-006-candidate-persistence-strategy.md",
)
REQUIRED_CODE_PATHS = (
"active-runtime/app/aios_v2/control_plane.py",
"active-runtime/app/aios_v2/persistence.py",
"active-runtime/app/aios_v2/gateway.py",
"active-runtime/app/aios_v2/app.py",
"active-runtime/app/aios_v2/orchestrator.py",
"active-runtime/app/aios_v2/execution_tracking.py",
"active-runtime/app/aios_v2/gitops_status.py",
"active-runtime/app/aios_v2/console.py",
"active-runtime/app/aios_openclaw/contract.py",
"active-runtime/app/aios_openclaw/adapter.py",
"active-runtime/app/aios_openclaw/transport.py",
"active-runtime/app/aios_openclaw/health.py",
"active-runtime/app/aios_openclaw/callback.py",
"active-runtime/app/aios_hermes/contract.py",
"active-runtime/app/aios_hermes/adapter.py",
"active-runtime/app/aios_hermes/worker.py",
"tests/aios_v2_candidate/test_candidate_flow.py",
"deploy/aios-v2/systemd/aios-v2.service",
"deploy/aios-v2/docker/Dockerfile",
"deploy/aios-v2/docker/compose.yaml",
"deploy/aios-v2/healthcheck/check.sh",
"deploy/aios-v2/rollback/rollback.sh",
"tasks/openclaw-production-deploy-task.yaml",
"tasks/codex-production-supervisor-task.yaml",
)
REQUIRED_APIS = (
"/api/v2/health",
"/api/v2/tenants",
"/api/v2/agents",
"/api/v2/tasks",
"/api/v2/audit",
"/api/v2/runtime",
"/api/v2/executions",
"/api/v2/gitops",
"/api/v2/hermes/results/reconcile",
)
REQUIRED_MODELS = (
"RequestContext",
"TenantRecord",
"AgentRecord",
"TaskRecord",
"TaskState",
"AuditEvent",
"OpenClawCommand",
"OpenClawResult",
"RuntimeHealth",
"HermesJob",
"HermesResult",
"HermesHealth",
"ExecutionEvidence",
)
CODE_CHANGE_PREFIXES = (
"active-runtime/app/aios_v2/",
"active-runtime/app/aios_openclaw/",
"active-runtime/app/aios_hermes/",
"deploy/aios-v2/",
"tests/aios_v2_candidate/",
"tasks/",
)
CONFLICT_MARKERS = ("<<<<<<<", "=======", ">>>>>>>")
def run_git(repo: Path, *args: str) -> tuple[int, str]:
process = subprocess.run(
["git", "-C", str(repo), *args],
text=True,
capture_output=True,
timeout=20,
)
return process.returncode, process.stdout.strip()
def chinese_ratio(text: str) -> float:
chinese = len(re.findall(r"[\u3400-\u9fff]", text))
latin = len(re.findall(r"[A-Za-z]", text))
denominator = chinese + latin
return chinese / denominator if denominator else 0.0
def all_text(paths: Iterable[Path]) -> str:
return "\n".join(path.read_text(encoding="utf-8") for path in paths)
def check_required_files(repo: Path, results: list[CheckResult]) -> list[Path]:
document_paths: list[Path] = []
for relative in REQUIRED_DOCUMENTS:
path = repo / relative
if not path.is_file():
results.append(CheckResult("错误", f"缺少必需文档:{relative}"))
continue
size = path.stat().st_size
if size < 500:
results.append(CheckResult("错误", f"文档正文过短:{relative}({size} 字节)"))
else:
document_paths.append(path)
for relative in REQUIRED_CODE_PATHS:
if not (repo / relative).is_file():
results.append(CheckResult("错误", f"文档引用的实现路径不存在:{relative}"))
return document_paths
def check_document_quality(paths: list[Path], results: list[CheckResult]) -> None:
for path in paths:
text = path.read_text(encoding="utf-8")
relative = path.as_posix()
if any(marker in text for marker in CONFLICT_MARKERS):
results.append(CheckResult("错误", f"文档存在未解决 Git 冲突标记:{relative}"))
ratio = chinese_ratio(text)
if ratio < 0.18:
results.append(
CheckResult(
"错误",
f"文档中文正文比例过低:{relative}({ratio:.1%})",
)
)
headings = re.findall(r"^#{1,6}\s+.+$", text, flags=re.MULTILINE)
if len(headings) < 3:
results.append(CheckResult("错误", f"文档章节不完整:{relative}"))
if re.search(r"(?mi)^\s*(TODO|TBD)\s*$", text):
results.append(CheckResult("错误", f"文档包含未完成占位符:{relative}"))
def check_requirements(repo: Path, results: list[CheckResult]) -> None:
requirements = (repo / "docs/aios-v2/01-USER-REQUIREMENTS.md").read_text(
encoding="utf-8"
)
matrix = (repo / "docs/aios-v2/05-TRACEABILITY-MATRIX.md").read_text(
encoding="utf-8"
)
for number in range(1, 16):
requirement = f"UR-{number:03d}"
if requirement not in requirements:
results.append(CheckResult("错误", f"需求文档缺少:{requirement}"))
if requirement not in matrix:
results.append(CheckResult("错误", f"追踪矩阵缺少:{requirement}"))
def check_api_and_models(repo: Path, docs_text: str, results: list[CheckResult]) -> None:
code_paths = [repo / path for path in REQUIRED_CODE_PATHS if path.endswith(".py")]
code_text = all_text(path for path in code_paths if path.is_file())
for api in REQUIRED_APIS:
if api not in docs_text:
results.append(CheckResult("错误", f"正式文档未覆盖核心 API:{api}"))
if api not in code_text:
results.append(CheckResult("错误", f"代码中未找到核心 API:{api}"))
for model in REQUIRED_MODELS:
if model not in docs_text:
results.append(CheckResult("错误", f"正式文档未覆盖核心模型:{model}"))
if model not in code_text:
results.append(CheckResult("错误", f"代码中未找到核心模型:{model}"))
def check_adr_links(repo: Path, results: list[CheckResult]) -> None:
index = (repo / "docs/aios-v2/adr/README.md").read_text(encoding="utf-8")
linked = re.findall(r"\((ADR-[^)]+\.md)\)", index)
if len(set(linked)) < 6:
results.append(CheckResult("错误", "ADR 索引未覆盖六项必需决策"))
for filename in linked:
if not (repo / "docs/aios-v2/adr" / filename).is_file():
results.append(CheckResult("错误", f"ADR 索引引用不存在:{filename}"))
def check_changed_docs(
repo: Path,
base_ref: str | None,
results: list[CheckResult],
) -> None:
if not base_ref:
results.append(CheckResult("警告", "未提供基准分支,跳过变更级文档同步检查"))
return
code, output = run_git(repo, "diff", "--name-only", f"{base_ref}...HEAD")
if code != 0:
results.append(CheckResult("警告", f"无法读取与 {base_ref} 的变更,跳过变更级检查"))
return
changed = [line.strip() for line in output.splitlines() if line.strip()]
code_changed = any(path.startswith(CODE_CHANGE_PREFIXES) for path in changed)
docs_changed = any(path.startswith("docs/aios-v2/") for path in changed)
if code_changed and not docs_changed:
results.append(
CheckResult(
"错误",
"检测到架构、功能、测试、任务或部署变化,但本交付范围没有 docs/aios-v2/ 变化",
)
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="AIOS V2 文档—代码匹配检查")
parser.add_argument("--repo", default=".", help="仓库根目录")
parser.add_argument(
"--base-ref",
default=None,
help="用于检查代码变化是否同步文档的基准引用",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
repo = Path(args.repo).resolve()
if not (repo / ".git").exists() and run_git(repo, "rev-parse", "--is-inside-work-tree")[0] != 0:
print(f"错误:不是 Git 仓库:{repo}", file=sys.stderr)
return 2
results: list[CheckResult] = []
document_paths = check_required_files(repo, results)
check_document_quality(document_paths, results)
check_requirements(repo, results)
docs_text = all_text(document_paths)
check_api_and_models(repo, docs_text, results)
check_adr_links(repo, results)
check_changed_docs(repo, args.base_ref, results)
errors = [item for item in results if item.level == "错误"]
warnings = [item for item in results if item.level == "警告"]
for item in results:
stream = sys.stderr if item.level == "错误" else sys.stdout
print(f"{item.level}:{item.message}", file=stream)
print(
f"文档—代码匹配检查完成:错误 {len(errors)},警告 {len(warnings)},正式文档 {len(document_paths)}。"
)
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())