#!/usr/bin/env python3 """Validate renderer API contract purity for key rendering components.""" from __future__ import annotations import json import re from pathlib import Path from typing import Any INCLUDE_RE = re.compile(r"""^\s*#\s*include\s+(\"([^\"]+)\"|<([^>]+)>)""") REPO_ROOT = Path(__file__).resolve().parents[2] CHECKS = { "renderer_api": { "roots": [REPO_ROOT / "src" / "renderer_api"], "allowed_include_prefixes": ("foundation/", "renderer_api/"), "forbidden_include_tokens": ( "renderer_gl/", "platform/", "platform_api/", "platform_", "opengl", "glad", "vulkan", "d3d", "directx", "metal", "appkit", "cocoa", "objc/", "windows.h", "x11/", "wayland-", "android/", ), "forbidden_body_tokens": ( "OpenGl", "OpenGL", "opengl_", "GL_", "Vulkan", "Vk", "MTL", "D3D", "vulkan", "metal", "renderer_gl", "pp_platform_", ), }, "paint_renderer": { "roots": [REPO_ROOT / "src" / "paint_renderer"], "allowed_include_prefixes": ( "assets/", "document/", "foundation/", "paint/", "paint_renderer/", "renderer_api/", ), "forbidden_include_tokens": ( "renderer_gl/", "platform/", "platform_api/", "platform_", "opengl", "glad", "vulkan", "d3d", "directx", "metal", "appkit", "cocoa", "objc/", "windows.h", "x11/", "wayland-", "android/", ), "forbidden_body_tokens": ( "OpenGl", "OpenGL", "opengl_", "GL_", "Vulkan", "Vk", "MTL", "D3D", "vulkan", "metal", "renderer_gl", "pp_platform_", ), }, } ALLOWED_EXTERNAL_PREFIXES = ("stb/",) def is_forbidden_include(allowlist: tuple[str, ...], forbidden_tokens: tuple[str, ...], include: str) -> tuple[bool, str | None]: include_lower = include.lower() if any(token in include_lower for token in forbidden_tokens): token = next(token for token in forbidden_tokens if token in include_lower) return True, token if "/" in include and include_lower.startswith(ALLOWED_EXTERNAL_PREFIXES): return False, None if "/" in include and not any(include_lower.startswith(prefix) for prefix in allowlist): return True, "cross-component-include" return False, None def scan_component(name: str, config: dict[str, Any]) -> list[dict[str, Any]]: violations: list[dict[str, Any]] = [] for root in config["roots"]: for path in root.rglob("*"): if not path.is_file(): continue if path.suffix.lower() not in {".cpp", ".cc", ".c", ".h", ".hpp", ".hh"}: continue text = path.read_text(encoding="utf-8").splitlines() for line_no, line in enumerate(text, start=1): include_match = INCLUDE_RE.match(line) if include_match: include = (include_match.group(2) or include_match.group(3) or "").strip() forbidden, reason = is_forbidden_include( config["allowed_include_prefixes"], config["forbidden_include_tokens"], include, ) if forbidden: violations.append( { "component": name, "file": str(path.relative_to(REPO_ROOT)), "line": line_no, "kind": "forbidden-include", "include": include, "detail": reason, "text": line.strip(), } ) joined = "\n".join(text) for token in config["forbidden_body_tokens"]: if token in joined: violations.append( { "component": name, "file": str(path.relative_to(REPO_ROOT)), "line": 0, "kind": "forbidden-body-token", "include": token, "detail": "backend- or platform-specific symbol in renderer contract path", "text": "", } ) return violations def main() -> int: violations: list[dict[str, Any]] = [] for component, config in CHECKS.items(): violations.extend(scan_component(component, config)) print( json.dumps( { "ok": len(violations) == 0, "summary": { "componentCount": len(CHECKS), "violationCount": len(violations), }, "violations": violations, }, separators=(",", ":"), ) ) return 0 if not violations else 1 if __name__ == "__main__": raise SystemExit(main())