docs: enforce source path policy for ouroboros plan links (#357)
This commit is contained in:
@@ -19,9 +19,20 @@ META_PATTERN = re.compile(
|
||||
re.MULTILINE,
|
||||
)
|
||||
ID_PATTERN = re.compile(r"\b(?:REQ|RULE|TASK|TEST|DOC)-[A-Z0-9-]+-\d{3}\b")
|
||||
DEF_PATTERN = re.compile(r"^-\s+`(?P<id>(?:REQ|RULE|TASK|TEST|DOC)-[A-Z0-9-]+-\d{3})`", re.MULTILINE)
|
||||
DEF_PATTERN = re.compile(
|
||||
r"^-\s+`(?P<id>(?:REQ|RULE|TASK|TEST|DOC)-[A-Z0-9-]+-\d{3})`",
|
||||
re.MULTILINE,
|
||||
)
|
||||
LINK_PATTERN = re.compile(r"\[[^\]]+\]\((?P<link>[^)]+)\)")
|
||||
LINE_DEF_PATTERN = re.compile(r"^-\s+`(?P<id>(?:REQ|RULE|TASK|TEST|DOC)-[A-Z0-9-]+-\d{3})`.*$", re.MULTILINE)
|
||||
LINE_DEF_PATTERN = re.compile(
|
||||
r"^-\s+`(?P<id>(?:REQ|RULE|TASK|TEST|DOC)-[A-Z0-9-]+-\d{3})`.*$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
PLAN_LINK_PATTERN = re.compile(r"ouroboros_plan_v(?P<version>[23])\.txt$")
|
||||
ALLOWED_PLAN_TARGETS = {
|
||||
"2": (DOC_DIR / "source" / "ouroboros_plan_v2.txt").resolve(),
|
||||
"3": (DOC_DIR / "source" / "ouroboros_plan_v3.txt").resolve(),
|
||||
}
|
||||
|
||||
|
||||
def iter_docs() -> list[Path]:
|
||||
@@ -40,11 +51,35 @@ def validate_metadata(path: Path, text: str, errors: list[str], doc_ids: dict[st
|
||||
doc_ids[doc_id] = path
|
||||
|
||||
|
||||
def validate_plan_source_link(path: Path, link: str, errors: list[str]) -> None:
|
||||
normalized = link.strip()
|
||||
match = PLAN_LINK_PATTERN.search(normalized)
|
||||
if not match:
|
||||
return
|
||||
|
||||
version = match.group("version")
|
||||
expected_target = ALLOWED_PLAN_TARGETS[version]
|
||||
if normalized.startswith("/"):
|
||||
errors.append(
|
||||
f"{path}: invalid plan link path -> {link} "
|
||||
f"(use ./source/ouroboros_plan_v{version}.txt)"
|
||||
)
|
||||
return
|
||||
|
||||
resolved_target = (path.parent / normalized).resolve()
|
||||
if resolved_target != expected_target:
|
||||
errors.append(
|
||||
f"{path}: invalid plan link path -> {link} "
|
||||
f"(must resolve to docs/ouroboros/source/ouroboros_plan_v{version}.txt)"
|
||||
)
|
||||
|
||||
|
||||
def validate_links(path: Path, text: str, errors: list[str]) -> None:
|
||||
for m in LINK_PATTERN.finditer(text):
|
||||
link = m.group("link").strip()
|
||||
if not link or link.startswith("http") or link.startswith("#"):
|
||||
continue
|
||||
validate_plan_source_link(path, link, errors)
|
||||
if link.startswith("/"):
|
||||
target = Path(link)
|
||||
else:
|
||||
@@ -61,7 +96,9 @@ def collect_ids(path: Path, text: str, defs: dict[str, Path], refs: dict[str, se
|
||||
refs.setdefault(idv, set()).add(path)
|
||||
|
||||
|
||||
def collect_req_traceability(text: str, req_to_task: dict[str, set[str]], req_to_test: dict[str, set[str]]) -> None:
|
||||
def collect_req_traceability(
|
||||
text: str, req_to_task: dict[str, set[str]], req_to_test: dict[str, set[str]]
|
||||
) -> None:
|
||||
for m in LINE_DEF_PATTERN.finditer(text):
|
||||
line = m.group(0)
|
||||
item_id = m.group("id")
|
||||
|
||||
53
tests/test_validate_ouroboros_docs.py
Normal file
53
tests/test_validate_ouroboros_docs.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_module():
|
||||
script_path = Path(__file__).resolve().parents[1] / "scripts" / "validate_ouroboros_docs.py"
|
||||
spec = importlib.util.spec_from_file_location("validate_ouroboros_docs", script_path)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_validate_plan_source_link_accepts_canonical_source_path() -> None:
|
||||
module = _load_module()
|
||||
errors: list[str] = []
|
||||
path = Path("docs/ouroboros/README.md").resolve()
|
||||
|
||||
module.validate_plan_source_link(path, "./source/ouroboros_plan_v2.txt", errors)
|
||||
module.validate_plan_source_link(path, "./source/ouroboros_plan_v3.txt", errors)
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_validate_plan_source_link_rejects_root_relative_path() -> None:
|
||||
module = _load_module()
|
||||
errors: list[str] = []
|
||||
path = Path("docs/ouroboros/README.md").resolve()
|
||||
|
||||
module.validate_plan_source_link(
|
||||
path,
|
||||
"/home/agentson/repos/The-Ouroboros/ouroboros_plan_v2.txt",
|
||||
errors,
|
||||
)
|
||||
|
||||
assert errors
|
||||
assert "invalid plan link path" in errors[0]
|
||||
assert "use ./source/ouroboros_plan_v2.txt" in errors[0]
|
||||
|
||||
|
||||
def test_validate_plan_source_link_rejects_repo_root_relative_path() -> None:
|
||||
module = _load_module()
|
||||
errors: list[str] = []
|
||||
path = Path("docs/ouroboros/README.md").resolve()
|
||||
|
||||
module.validate_plan_source_link(path, "../../ouroboros_plan_v2.txt", errors)
|
||||
|
||||
assert errors
|
||||
assert "invalid plan link path" in errors[0]
|
||||
assert "must resolve to docs/ouroboros/source/ouroboros_plan_v2.txt" in errors[0]
|
||||
Reference in New Issue
Block a user