Add renderer and package readiness validation gates

This commit is contained in:
2026-06-15 19:20:56 +02:00
parent 68617e8bc4
commit f78fc3076c
23 changed files with 2350 additions and 389 deletions

View File

@@ -0,0 +1,188 @@
#!/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())