diff --git a/scripts/validate_ouroboros_docs.py b/scripts/validate_ouroboros_docs.py index 9f1485e..902aded 100755 --- a/scripts/validate_ouroboros_docs.py +++ b/scripts/validate_ouroboros_docs.py @@ -51,27 +51,33 @@ 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: +def validate_plan_source_link(path: Path, link: str, errors: list[str]) -> bool: normalized = link.strip() - match = PLAN_LINK_PATTERN.search(normalized) + # Ignore in-page anchors and parse the filesystem part for validation. + link_path = normalized.split("#", 1)[0].strip() + if not link_path: + return False + match = PLAN_LINK_PATTERN.search(link_path) if not match: - return + return False version = match.group("version") expected_target = ALLOWED_PLAN_TARGETS[version] - if normalized.startswith("/"): + if link_path.startswith("/"): errors.append( f"{path}: invalid plan link path -> {link} " f"(use ./source/ouroboros_plan_v{version}.txt)" ) - return + return True - resolved_target = (path.parent / normalized).resolve() + resolved_target = (path.parent / link_path).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)" ) + return True + return False def validate_links(path: Path, text: str, errors: list[str]) -> None: @@ -79,11 +85,13 @@ def validate_links(path: Path, text: str, errors: list[str]) -> None: 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) + if validate_plan_source_link(path, link, errors): + continue + link_path = link.split("#", 1)[0].strip() + if link_path.startswith("/"): + target = Path(link_path) else: - target = (path.parent / link).resolve() + target = (path.parent / link_path).resolve() if not target.exists(): errors.append(f"{path}: broken link -> {link}") diff --git a/tests/test_validate_ouroboros_docs.py b/tests/test_validate_ouroboros_docs.py index 66c0ec5..3ef972b 100644 --- a/tests/test_validate_ouroboros_docs.py +++ b/tests/test_validate_ouroboros_docs.py @@ -19,8 +19,8 @@ def test_validate_plan_source_link_accepts_canonical_source_path() -> None: 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 module.validate_plan_source_link(path, "./source/ouroboros_plan_v2.txt", errors) is False + assert module.validate_plan_source_link(path, "./source/ouroboros_plan_v3.txt", errors) is False assert errors == [] @@ -30,12 +30,13 @@ def test_validate_plan_source_link_rejects_root_relative_path() -> None: errors: list[str] = [] path = Path("docs/ouroboros/README.md").resolve() - module.validate_plan_source_link( + handled = module.validate_plan_source_link( path, "/home/agentson/repos/The-Ouroboros/ouroboros_plan_v2.txt", errors, ) + assert handled is True assert errors assert "invalid plan link path" in errors[0] assert "use ./source/ouroboros_plan_v2.txt" in errors[0] @@ -46,8 +47,35 @@ def test_validate_plan_source_link_rejects_repo_root_relative_path() -> None: errors: list[str] = [] path = Path("docs/ouroboros/README.md").resolve() - module.validate_plan_source_link(path, "../../ouroboros_plan_v2.txt", errors) + handled = module.validate_plan_source_link(path, "../../ouroboros_plan_v2.txt", errors) + assert handled is True assert errors assert "invalid plan link path" in errors[0] assert "must resolve to docs/ouroboros/source/ouroboros_plan_v2.txt" in errors[0] + + +def test_validate_plan_source_link_accepts_fragment_suffix() -> None: + module = _load_module() + errors: list[str] = [] + path = Path("docs/ouroboros/README.md").resolve() + + handled = module.validate_plan_source_link(path, "./source/ouroboros_plan_v2.txt#sec", errors) + + assert handled is False + assert errors == [] + + +def test_validate_links_avoids_duplicate_error_for_invalid_plan_link(tmp_path) -> None: + module = _load_module() + errors: list[str] = [] + doc = tmp_path / "doc.md" + doc.write_text( + "[v2](/home/agentson/repos/The-Ouroboros/ouroboros_plan_v2.txt)\n", + encoding="utf-8", + ) + + module.validate_links(doc, doc.read_text(encoding="utf-8"), errors) + + assert len(errors) == 1 + assert "invalid plan link path" in errors[0]