Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions validation/engines/python_checks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ._types import CheckDescriptor, CheckScope

from .error_code_checks import check_conflict_deprecated, check_contextcode_format
from .externaldocs_checks import check_externaldocs
from .filename_checks import check_filename_kebab_case, check_filename_matches_api_name
from .info_description_checks import check_info_description_templates
from .metadata_checks import check_commonalities_version
Expand Down Expand Up @@ -70,6 +71,7 @@
CheckScope.API,
check_info_description_templates,
),
CheckDescriptor("check-externaldocs-repository", CheckScope.API, check_externaldocs),
# --- Repo-level checks (run once) ---
CheckDescriptor("check-test-directory-exists", CheckScope.REPO, check_test_directory_exists),
CheckDescriptor("check-release-plan-semantics", CheckScope.REPO, check_release_plan_semantics),
Expand Down
110 changes: 110 additions & 0 deletions validation/engines/python_checks/externaldocs_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""externalDocs repository and description checks (Design Guide §5.4).

Design Guide §5.4 (ExternalDocs Object) is a hard SHALL: ``externalDocs.url``
must be ``https://github.com/camaraproject/{apiRepository}`` for the
repository hosting the API, and ``externalDocs.description`` must read
"Product documentation at CAMARA". No carve-out for intentional
cross-repository references — a full sweep of upstream/apis/* found zero
legitimate cases; every mismatch was a stale copy-paste or repo rename.
"""

from __future__ import annotations

from pathlib import Path
from typing import List

from validation.context import ValidationContext

from ._types import load_yaml_safe, make_finding

_URL_ENGINE_RULE = "check-externaldocs-repository"
_DESCRIPTION_ENGINE_RULE = "check-externaldocs-description"

_EXPECTED_DESCRIPTION = "Product documentation at CAMARA"


def check_externaldocs(
repo_path: Path, context: ValidationContext
) -> List[dict]:
"""Validate externalDocs.url and externalDocs.description.

Per-API check. Emits two distinct engine_rule values from the same
externalDocs node so the postfilter can give them different
severities (P-038 warn, P-039 hint):

- P-038 (check-externaldocs-repository): externalDocs missing
entirely, or url is not exactly
https://github.com/camaraproject/{repo-name}.
- P-039 (check-externaldocs-description): description is not
exactly "Product documentation at CAMARA". Only checked when
externalDocs is present — a missing object is a single P-038
finding, not P-038 + P-039.

Match is strict exact-string (no trailing-slash tolerance, no
case-insensitive description match) — matches the Design Guide
template literally.
"""
api = context.apis[0]
spec_path = repo_path / api.spec_file
spec = load_yaml_safe(spec_path)

if spec is None:
return []

repo_name = context.repository.rsplit("/", 1)[-1]
expected_url = f"https://github.com/camaraproject/{repo_name}"

external_docs = spec.get("externalDocs")

if not isinstance(external_docs, dict):
return [
make_finding(
engine_rule=_URL_ENGINE_RULE,
level="warn",
message=(
f"externalDocs is missing in {api.spec_file} — "
f"expected url '{expected_url}'"
),
path=api.spec_file,
line=1,
api_name=api.api_name,
)
]

findings: List[dict] = []

url = external_docs.get("url")
if url != expected_url:
actual = "is missing" if url is None else f"is '{url}'"
findings.append(
make_finding(
engine_rule=_URL_ENGINE_RULE,
level="warn",
message=(
f"externalDocs.url in {api.spec_file} {actual} — "
f"expected '{expected_url}'"
),
path=api.spec_file,
line=1,
api_name=api.api_name,
)
)

description = external_docs.get("description")
if description != _EXPECTED_DESCRIPTION:
actual = "is missing" if description is None else f"is '{description}'"
findings.append(
make_finding(
engine_rule=_DESCRIPTION_ENGINE_RULE,
level="hint",
message=(
f"externalDocs.description in {api.spec_file} {actual} "
f"— expected '{_EXPECTED_DESCRIPTION}'"
),
path=api.spec_file,
line=1,
api_name=api.api_name,
)
)

return findings
32 changes: 32 additions & 0 deletions validation/rules/python-rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -596,3 +596,35 @@
Prefer block-style YAML for examples. Rewrite invalid JSON-like flow-style
YAML mappings or sequences so the file parses with YAML 1.2-conformant
parsers.

# P-038: check-externaldocs-repository (DG §5.4)
# externalDocs must reference the canonical CAMARA GitHub repository for
# this API. Fires when externalDocs is missing entirely, or url is not
# exactly https://github.com/camaraproject/{repo-name}. No carve-out for
# intentional cross-repository references — Design Guide §5.4 is a hard
# SHALL and a full upstream/apis/* sweep found zero legitimate cross-repo
# cases; every mismatch was a stale copy-paste or repo rename.
- id: P-038
engine: python
engine_rule: check-externaldocs-repository
short_title: "externalDocs.url must reference this repository"
conditional_level:
default: warn
suggestion: >-
Set externalDocs.url to https://github.com/camaraproject/{repo-name},
replacing {repo-name} with this repository's actual name.

# P-039: check-externaldocs-description (DG §5.4)
# externalDocs.description must exactly match the Design Guide template
# text. Satellite rule emitted by check_externaldocs() alongside P-038 —
# only fires when externalDocs is present at all (a missing object is a
# single P-038 finding, not P-038 + P-039).
- id: P-039
engine: python
engine_rule: check-externaldocs-description
short_title: "externalDocs.description must match the DG template"
conditional_level:
default: hint
suggestion: >-
Set externalDocs.description to exactly "Product documentation at
CAMARA".
Loading