106 lines
5.4 KiB
Python
Executable File
106 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# file: scripts/audit_project_workspace_rules.py
|
|
# version: 5
|
|
|
|
"""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()}")
|
|
history_root = root / "history"
|
|
if history_root.exists():
|
|
for history_path in sorted(history_root.rglob("*.md")):
|
|
relative_history = history_path.relative_to(history_root)
|
|
if relative_history.as_posix() == "000-README.md":
|
|
continue
|
|
if len(relative_history.parts) != 2:
|
|
errors.append(f"DOC-010: history file must use history/<X.Y.Z>/<label>.md: {history_path.relative_to(root).as_posix()}")
|
|
continue
|
|
base_version, filename = relative_history.parts
|
|
if re.fullmatch(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)", base_version) is None:
|
|
errors.append(f"DOC-010: invalid history base version directory: {history_path.relative_to(root).as_posix()}")
|
|
label = filename[:-3]
|
|
candidate = f"{base_version}-{label}"
|
|
if SEMVER.fullmatch(candidate) is None:
|
|
errors.append(f"DOC-010: invalid history milestone filename: {history_path.relative_to(root).as_posix()}")
|
|
|
|
docs_root = root / "docs"
|
|
if docs_root.exists():
|
|
for readme_path in sorted(docs_root.rglob("README.md")):
|
|
errors.append(
|
|
f"DOC-011: documentary directory entry point must use 000-README.md: {readme_path.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())
|