Files
games/scripts/audit_project_workspace_rules.py
2026-09-16 01:01:09 +02:00

83 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
# file: scripts/audit_project_workspace_rules.py
# version: 3
"""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")
for source_path in sorted((root / "crates").rglob("src/**/*.rs")):
relative = source_path.relative_to(root).as_posix()
text = source_path.read_text(encoding="utf-8")
if "#[test]" in text:
errors.append(f"RUST-TEST-001: test body forbidden under src/: {relative}")
if "#[cfg(test)]" in text:
external_test_module = re.search(
r'#\[cfg\(test\)\]\s*#\[path\s*=\s*"[^"]*unit_tests/[^"]+"\]\s*mod\s+tests\s*;',
text,
re.MULTILINE,
)
if external_test_module is None:
errors.append(f"RUST-TEST-001: cfg(test) under src/ must reference an external unit_tests file: {relative}")
for test_dir in sorted((root / "crates").rglob("src/**/unit_tests")):
if test_dir.is_dir():
errors.append(f"RUST-TEST-002: unit_tests directory forbidden under src/: {test_dir.relative_to(root).as_posix()}")
for test_dir in sorted((root / "crates").rglob("src/**/tests")):
if test_dir.is_dir():
errors.append(f"RUST-TEST-003: integration tests directory forbidden under src/: {test_dir.relative_to(root).as_posix()}")
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())