#!/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())