문서 중복·드리프트를 구조적으로 방지하기 위해 SSOT 원칙을 문서 체계에 적용한다. 신규: - docs/README.md: 문서 라우팅/역할/읽기 순서/SSOT 정의 (상대경로 링크) - scripts/validate_docs_sync.py: 가변 수치 하드코딩 금지 + 누락 엔드포인트 검사 수정: - CLAUDE.md: 문서 진입점 추가, SmartScanner 세부 동작 → architecture.md 링크 - README.md: 문서 네비게이션 섹션 추가, 고정 수치/파일별 케이스 수 제거 - docs/commands.md: validate_docs_sync.py 명령 추가; 중복 엔드포인트 2행 제거 - docs/testing.md: 테스트 총량 고정값 → pytest --collect-only -q 동적 확인으로 전환 - docs/ouroboros/82_doc_restructure_plan.md: draft → active, 실행 현황으로 전환 - .gitea/PULL_REQUEST_TEMPLATE.md: Docs Sync 체크리스트 추가 - .gitea/workflows/ci.yml + .github/workflows/ci.yml: validate_docs_sync 단계 추가 검증: - python3 scripts/validate_docs_sync.py: PASS Closes #350
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate lightweight documentation synchronization rules."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(".")
|
|
|
|
|
|
def read_text(path: Path, errors: list[str]) -> str:
|
|
if not path.exists():
|
|
errors.append(f"missing file: {path}")
|
|
return ""
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def main() -> int:
|
|
errors: list[str] = []
|
|
|
|
docs_index = ROOT / "docs" / "README.md"
|
|
readme = ROOT / "README.md"
|
|
claude = ROOT / "CLAUDE.md"
|
|
commands = ROOT / "docs" / "commands.md"
|
|
|
|
docs_index_text = read_text(docs_index, errors)
|
|
readme_text = read_text(readme, errors)
|
|
claude_text = read_text(claude, errors)
|
|
commands_text = read_text(commands, errors)
|
|
|
|
if docs_index_text and "Single Source of Truth" not in docs_index_text:
|
|
errors.append("docs/README.md: missing 'Single Source of Truth' section")
|
|
|
|
if readme_text and "docs/README.md" not in readme_text:
|
|
errors.append("README.md: missing docs/README.md routing link")
|
|
if claude_text and "docs/README.md" not in claude_text:
|
|
errors.append("CLAUDE.md: missing docs/README.md routing link")
|
|
|
|
# Prevent volatile hard-coded scale numbers in summary docs.
|
|
volatile_patterns: list[tuple[str, re.Pattern[str]]] = [
|
|
("README.md", re.compile(r"\b\d+\s*개 테스트\b")),
|
|
("README.md", re.compile(r"\b\d+\s*tests\s+across\b", re.IGNORECASE)),
|
|
("README.md", re.compile(r"\(\d+\s*개 API\)")),
|
|
("README.md", re.compile(r"\(\d+\s*API endpoints?\)", re.IGNORECASE)),
|
|
("CLAUDE.md", re.compile(r"\b\d+\s*tests\s+across\b", re.IGNORECASE)),
|
|
("CLAUDE.md", re.compile(r"\(\d+\s*API endpoints?\)", re.IGNORECASE)),
|
|
("docs/commands.md", re.compile(r"Run full test suite with coverage\s*\(\d+", re.IGNORECASE)),
|
|
]
|
|
text_by_name = {
|
|
"README.md": readme_text,
|
|
"CLAUDE.md": claude_text,
|
|
"docs/commands.md": commands_text,
|
|
}
|
|
for name, pattern in volatile_patterns:
|
|
text = text_by_name.get(name, "")
|
|
if text and pattern.search(text):
|
|
errors.append(f"{name}: contains volatile hard-coded scale text ({pattern.pattern})")
|
|
|
|
# Command doc should list all dashboard endpoints exposed by app.py.
|
|
for endpoint in ("/api/pnl/history", "/api/positions"):
|
|
if commands_text and endpoint not in commands_text:
|
|
errors.append(f"docs/commands.md: missing dashboard endpoint {endpoint}")
|
|
|
|
if errors:
|
|
print("[FAIL] docs sync validation failed")
|
|
for err in errors:
|
|
print(f"- {err}")
|
|
return 1
|
|
|
|
print("[OK] docs sync validation passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|