Files
games/scripts/audit_project_workspace_rules.py
2026-09-15 23:26:37 +02:00

64 lines
2.9 KiB
Python
Executable File

#!/usr/bin/env python3
# file: scripts/audit_project_workspace_rules.py
# version: 2
"""Audit mechanically verifiable games.sasedev workspace boundaries."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
import tomllib
SEMVER = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0-pre|1-alpha|2-beta|3-rc)\.[1-9][0-9]*(?:\.fix\.[1-9][0-9]*)?)?$", re.ASCII)
def main() -> int:
"""Run project-specific workspace audits."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".", help="workspace root")
arguments = parser.parse_args()
root = pathlib.Path(arguments.root).resolve()
errors: list[str] = []
manifest = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8"))
workspace_version = manifest.get("workspace", {}).get("package", {}).get("version")
if not isinstance(workspace_version, str) or SEMVER.fullmatch(workspace_version) is None:
errors.append("VERSION-001: workspace.package.version does not follow the canonical games.sasedev SemVer scheme")
members = manifest.get("workspace", {}).get("members", [])
for member in members:
if not isinstance(member, str) or not member.startswith("crates/"):
errors.append(f"GAME-WS-002: workspace member must be under crates/: {member!r}")
for manifest_path in sorted((root / "crates").rglob("Cargo.toml")):
relative = manifest_path.relative_to(root).as_posix()
data = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
package = data.get("package", {})
version = package.get("version")
if isinstance(version, dict):
if version.get("workspace") is not True:
errors.append(f"GAME-WS-004: {relative}: table version must use workspace = true")
elif isinstance(version, str):
if SEMVER.fullmatch(version) is None:
errors.append(f"GAME-WS-005: {relative}: explicit crate version does not follow the canonical games.sasedev SemVer scheme")
else:
errors.append(f"GAME-WS-004: {relative}: crate must inherit or explicitly own a version")
forbidden_assets = [path for path in (root / "crates").rglob("assets") if path.is_dir()]
for path in forbidden_assets:
errors.append(f"GAME-ASSET-001: assets directory forbidden inside crates: {path.relative_to(root).as_posix()}")
for java_path in sorted(root.rglob("*.java")):
if not java_path.is_relative_to(root / "Android"):
errors.append(f"GAME-ANDROID-001: Java source outside Android/: {java_path.relative_to(root).as_posix()}")
if errors:
for error in errors:
print(error, file=sys.stderr)
print(f"games.sasedev workspace audit: {len(errors)} violation(s)", file=sys.stderr)
return 1
print("games.sasedev workspace audit: clean")
return 0
if __name__ == "__main__":
raise SystemExit(main())