from __future__ import annotations
import argparse
import glob
import sys
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
ROOT = Path(__file__).resolve().parent.parent
ROOT_PACKAGE = "condor-for-games"
CORE = "condor-pathfinding-core"
GEOMETRY = "condor-pathfinding-geometry"
GRID = "condor-pathfinding-grid"
NAVMESH = "condor-pathfinding-navmesh"
HARNESS = "condor-harness"
BENCH = "condor-bench"
LAB = "condor-lab"
PATH_LAB = "condor-path-lab"
TUI = "condor-tui"
ALLOWED_LOCAL_DEPENDENCIES = {
ROOT_PACKAGE: frozenset({CORE, GEOMETRY, GRID, NAVMESH}),
CORE: frozenset(),
GEOMETRY: frozenset({CORE}),
GRID: frozenset({CORE}),
NAVMESH: frozenset({CORE, GEOMETRY}),
HARNESS: frozenset({CORE, GEOMETRY, GRID, NAVMESH}),
BENCH: frozenset({HARNESS, CORE, GEOMETRY, GRID, NAVMESH}),
TUI: frozenset(),
LAB: frozenset({BENCH, TUI}),
PATH_LAB: frozenset({BENCH, GEOMETRY, GRID, NAVMESH, TUI}),
}
@dataclass(frozen=True, order=True)
class Edge:
source: str
kind: str
target: str
def render(self) -> str:
return f"{self.source} --{self.kind}--> {self.target}"
EXPECTED_CURRENT_VIOLATIONS = frozenset(
{
Edge(HARNESS, "dev", ROOT_PACKAGE),
Edge(BENCH, "dev", ROOT_PACKAGE),
}
)
REQUIRED_LOCAL_EDGES = frozenset(
{
Edge(LAB, "normal", BENCH),
}
)
POLYGON_PACK_PAIRS = (
(
ROOT / "src/polygonal/packs/starter.toml",
ROOT / "dev/condor-harness/fixtures/polygonal/starter.toml",
),
(
ROOT / "src/polygonal/packs/stress.toml",
ROOT / "dev/condor-harness/fixtures/polygonal/stress.toml",
),
)
def load_toml(path: Path) -> dict[str, Any]:
with path.open("rb") as handle:
return tomllib.load(handle)
def workspace_manifests() -> list[Path]:
root_manifest = ROOT / "Cargo.toml"
root_data = load_toml(root_manifest)
workspace = root_data.get("workspace", {})
manifests = {root_manifest.resolve()}
for member in workspace.get("members", []):
pattern = str(ROOT / member / "Cargo.toml")
matches = [Path(path).resolve() for path in glob.glob(pattern)]
if not matches:
raise ValueError(f"workspace member does not resolve: {member}")
manifests.update(matches)
return sorted(manifests)
def dependency_tables(data: dict[str, Any]) -> Iterable[tuple[str, dict[str, Any]]]:
sections = (
("normal", "dependencies"),
("dev", "dev-dependencies"),
("build", "build-dependencies"),
)
for kind, section in sections:
yield kind, data.get(section, {})
for target in data.get("target", {}).values():
if not isinstance(target, dict):
continue
for kind, section in sections:
yield kind, target.get(section, {})
def dependency_path(
alias: str,
declaration: Any,
manifest_dir: Path,
workspace_dependencies: dict[str, Any],
) -> Path | None:
if not isinstance(declaration, dict):
return None
inherited = declaration.get("workspace") is True
effective = workspace_dependencies.get(alias, {}) if inherited else declaration
if not isinstance(effective, dict) or "path" not in effective:
return None
base = ROOT if inherited else manifest_dir
return (base / effective["path"] / "Cargo.toml").resolve()
def read_graph() -> tuple[set[str], set[Edge]]:
root_data = load_toml(ROOT / "Cargo.toml")
workspace_dependencies = root_data.get("workspace", {}).get("dependencies", {})
manifests = workspace_manifests()
package_by_manifest: dict[Path, str] = {}
data_by_manifest: dict[Path, dict[str, Any]] = {}
for manifest in manifests:
data = load_toml(manifest)
data_by_manifest[manifest] = data
package_by_manifest[manifest] = data["package"]["name"]
edges: set[Edge] = set()
for manifest, data in data_by_manifest.items():
source = package_by_manifest[manifest]
for kind, dependencies in dependency_tables(data):
for alias, declaration in dependencies.items():
target_manifest = dependency_path(
alias, declaration, manifest.parent, workspace_dependencies
)
if target_manifest is None or not target_manifest.is_relative_to(ROOT):
continue
if target_manifest not in package_by_manifest:
if not target_manifest.is_file():
raise ValueError(
f"local dependency manifest does not exist: {target_manifest}"
)
package_by_manifest[target_manifest] = load_toml(target_manifest)[
"package"
]["name"]
edges.add(Edge(source, kind, package_by_manifest[target_manifest]))
return set(data_by_manifest[path]["package"]["name"] for path in manifests), edges
def graph_violations(packages: set[str], edges: set[Edge]) -> tuple[list[str], set[Edge]]:
unknown_packages = sorted(packages - ALLOWED_LOCAL_DEPENDENCIES.keys())
violations = {
edge
for edge in edges
if edge.target not in ALLOWED_LOCAL_DEPENDENCIES.get(edge.source, frozenset())
}
return unknown_packages, violations
def print_edges(label: str, edges: Iterable[Edge]) -> None:
rendered = list(sorted(edges))
if not rendered:
return
print(label, file=sys.stderr)
for edge in rendered:
print(f" - {edge.render()}", file=sys.stderr)
def check_package(package: str, packages: set[str], violations: set[Edge]) -> int:
if package not in packages:
print(f"architecture-check-package: unknown workspace package: {package}", file=sys.stderr)
return 2
if package not in ALLOWED_LOCAL_DEPENDENCIES:
print(
f"architecture-check-package: package has no declared architecture role: {package}",
file=sys.stderr,
)
return 1
package_violations = {edge for edge in violations if edge.source == package}
if package_violations:
print_edges(
f"architecture-check-package: prohibited local dependencies for {package}:",
package_violations,
)
return 1
print(f"architecture-check-package: {package} conforms")
return 0
def check_expected_violations(
label: str, unknown: list[str], violations: set[Edge]
) -> int:
if unknown:
print(
f"{label}: packages without declared roles: " + ", ".join(unknown),
file=sys.stderr,
)
return 1
unexpected = violations - EXPECTED_CURRENT_VIOLATIONS
missing = EXPECTED_CURRENT_VIOLATIONS - violations
if unexpected or missing:
print_edges(
f"{label}: unexpected prohibited dependencies:",
unexpected,
)
print_edges(
f"{label}: expected deferred dependencies not found:",
missing,
)
return 1
print(
f"{label}: exact deferred facade-edge set confirmed "
f"({len(violations)} edges)"
)
return 0
def check_required_edges(label: str, edges: set[Edge]) -> int:
missing = REQUIRED_LOCAL_EDGES - edges
if missing:
print_edges(f"{label}: required local dependencies not found:", missing)
return 1
return 0
def check_polygon_pack_parity(label: str) -> int:
mismatches = [
(root_pack, harness_pack)
for root_pack, harness_pack in POLYGON_PACK_PAIRS
if not root_pack.is_file()
or not harness_pack.is_file()
or root_pack.read_bytes() != harness_pack.read_bytes()
]
if mismatches:
print(
f"{label}: harness polygon-pack copies differ from root public packs:",
file=sys.stderr,
)
for root_pack, harness_pack in mismatches:
print(f" - {root_pack.relative_to(ROOT)} != {harness_pack.relative_to(ROOT)}", file=sys.stderr)
return 1
return 0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="mode", required=True)
package = subparsers.add_parser("package")
package.add_argument("name")
subparsers.add_parser("transitional")
subparsers.add_parser("final")
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
packages, edges = read_graph()
unknown, violations = graph_violations(packages, edges)
except (OSError, KeyError, TypeError, ValueError, tomllib.TOMLDecodeError) as error:
print(f"architecture-check: unable to read workspace graph: {error}", file=sys.stderr)
return 2
if args.mode == "package":
return check_package(args.name, packages, violations)
label = (
"architecture-check-transitional"
if args.mode == "transitional"
else "architecture-check"
)
if check_expected_violations(label, unknown, violations):
return 1
if check_required_edges(label, edges):
return 1
if check_polygon_pack_parity(label):
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())