docs validator: enforce source path policy for ouroboros plan links (#357) #360
@@ -19,9 +19,20 @@ META_PATTERN = re.compile(
|
|||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
ID_PATTERN = re.compile(r"\b(?:REQ|RULE|TASK|TEST|DOC)-[A-Z0-9-]+-\d{3}\b")
|
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>[^)]+)\)")
|
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]:
|
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
|
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:
|
def validate_links(path: Path, text: str, errors: list[str]) -> None:
|
||||||
for m in LINK_PATTERN.finditer(text):
|
for m in LINK_PATTERN.finditer(text):
|
||||||
link = m.group("link").strip()
|
link = m.group("link").strip()
|
||||||
if not link or link.startswith("http") or link.startswith("#"):
|
if not link or link.startswith("http") or link.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
validate_plan_source_link(path, link, errors)
|
||||||
if link.startswith("/"):
|
if link.startswith("/"):
|
||||||
target = Path(link)
|
target = Path(link)
|
||||||
else:
|
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)
|
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):
|
for m in LINE_DEF_PATTERN.finditer(text):
|
||||||
line = m.group(0)
|
line = m.group(0)
|
||||||
item_id = m.group("id")
|
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