#!/usr/bin/env python3 """AM-4: third-party dependency weight, measured as source under audit. Crate count is a poor cross-ecosystem proxy — Rust splits crates far more finely than npm, so "33 crates vs 120 npm packages" flatters us in one direction and a low crate-count target punishes us in the other. What the count stands in for is how much third-party source a reviewer would have to audit. This measures that directly, in two configurations: shipped-runtime cargo build --no-default-features (what a game ships) dev-toolchain cargo build (adds scenario YAML) Positive control (InnerLoop v1.0 §Step 5): every crate in the dependency graph must be located on disk and produce a non-zero line count. A crate that cannot be found is reported and the run exits non-zero rather than silently under-reporting the total — under-reporting is the exact direction this metric could be gamed. Usage: python3 tools/dep-weight.py [--json] """ import glob import json import os import shutil import subprocess import sys PACKAGE = "games-ground" CONFIGS = { "shipped-runtime": ["--no-default-features"], "dev-toolchain": [], } # AM-4a / AM-4b targets from specs/GameKernel.md §4. Breaching one fails # the build: a gate that only reports is a suggestion. TARGETS = { "shipped-runtime": 250_000, "dev-toolchain": 350_000, } def crates(extra_args): """Third-party crates in the normal (non-dev) dependency graph.""" if not shutil.which("cargo"): print( "ERROR: cargo not on PATH. Try: export PATH=\"$HOME/.cargo/bin:$PATH\"", file=sys.stderr, ) sys.exit(1) out = subprocess.run( ["cargo", "tree", "-p", PACKAGE, "--edges", "normal", "--prefix", "none"] + extra_args, capture_output=True, text=True, check=True, ).stdout found = {} for line in out.splitlines(): parts = line.split() if len(parts) < 2 or not parts[1].startswith("v"): continue name, version = parts[0], parts[1].lstrip("v") # Path dependencies are our own code, not third-party. if "(/" in line: continue found[name] = version return found def source_lines(name, version): """Lines of Rust in the vendored source for one crate.""" roots = glob.glob(os.path.expanduser("~/.cargo/registry/src/*/")) for root in roots: # Version may carry a build suffix (e.g. 0.9.34+deprecated). for d in glob.glob(f"{root}{name}-{version}*/") + glob.glob(f"{root}{name}-*/"): total = 0 for dirpath, _, files in os.walk(d): for f in files: if f.endswith(".rs"): try: with open(os.path.join(dirpath, f), "rb") as fh: total += fh.read().count(b"\n") except OSError: pass if total: return total return 0 def main(): report = {} missing = [] for label, args in CONFIGS.items(): found = crates(args) per_crate = {} for name, version in sorted(found.items()): lines = source_lines(name, version) if lines == 0: missing.append(f"{name} {version} ({label})") per_crate[name] = lines report[label] = { "crates": len(found), "third_party_loc": sum(per_crate.values()), "per_crate": per_crate, } own = 0 for base in ("crates", "games", "tools"): for dirpath, _, files in os.walk(base): if "target" in dirpath.split(os.sep): continue for f in files: if f.endswith(".rs"): with open(os.path.join(dirpath, f), "rb") as fh: own += fh.read().count(b"\n") report["own_loc"] = own if "--json" in sys.argv: print(json.dumps(report, indent=2)) else: print("AM-4 dependency weight") print(f" own source {own:>9,} lines") for label in CONFIGS: r = report[label] limit = TARGETS[label] mark = "ok " if r["third_party_loc"] <= limit else "FAIL" print( f" {label:<18}{r['crates']:>3} crates " f"{r['third_party_loc']:>9,} lines third-party " f"[{mark} target {limit:,}]" ) delta = ( report["dev-toolchain"]["third_party_loc"] - report["shipped-runtime"]["third_party_loc"] ) print(f" scenario tooling costs {delta:>9,} lines (dev only)") if missing: # Positive control: a crate we could not measure would silently # shrink the total, so refuse to report rather than under-report. print("\nERROR — source not found for:", ", ".join(missing), file=sys.stderr) return 1 breached = [ (label, report[label]["third_party_loc"], limit) for label, limit in TARGETS.items() if report[label]["third_party_loc"] > limit ] for label, actual, limit in breached: print( f"\nFAIL AM-4 — {label}: {actual:,} lines exceeds target {limit:,}", file=sys.stderr, ) return 1 if breached else 0 if __name__ == "__main__": sys.exit(main())