AM-4: gate scenario YAML, retarget on audited source, re-measure
Some checks failed
ci / check (push) Failing after 3s
Some checks failed
ci / check (push) Failing after 3s
Adopts both remediations from CB-EV-0001 §4 (maintainer decision). Option A — serde_yaml is now optional behind cb-game-runtime's `scenarios` feature. The scenario module, the ScenarioGame impl and the string parsers behind it are cfg-gated; cb-sim opts in explicitly. Both configurations compile and lint clean under -D warnings. A trap worth recording: `default-features = false` on a *member* dependency is silently ignored when the workspace dependency does not specify it. The first attempt gated nothing while looking correct — the build succeeded and cargo tree still showed all six YAML crates. Fixed by setting it on the workspace dependency. This is the positive-control failure mode in miniature: success was not evidence the change applied. Retarget — AM-4 now measures third-party source under audit, split by build configuration, replacing a crate count that was unreachable without undoing K5/K7 and that does not compare across ecosystems. Re-measured via the new `make dep-weight`, whose own positive control refuses to report when any crate's source cannot be located: shipped runtime 23 crates 246,250 lines target <=250,000 met dev toolchain 29 crates 317,021 lines target <=350,000 met own source 3,408 lines Scenario tooling costs 70,771 lines a shipped game never compiles — the split the single number was hiding. Targets are set at current measurement plus headroom, so they bind on future growth rather than retroactively passing what had failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8e11fc412e
commit
4be6e020ea
12 changed files with 271 additions and 43 deletions
|
|
@ -5,8 +5,8 @@ version.workspace = true
|
|||
license-file.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cb-game-runtime.workspace = true
|
||||
games-ground.workspace = true
|
||||
cb-game-runtime = { workspace = true, features = ["scenarios"] }
|
||||
games-ground = { workspace = true, features = ["scenarios"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
138
tools/dep-weight.py
Executable file
138
tools/dep-weight.py
Executable file
|
|
@ -0,0 +1,138 @@
|
|||
#!/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": [],
|
||||
}
|
||||
|
||||
|
||||
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]
|
||||
print(
|
||||
f" {label:<18}{r['crates']:>3} crates "
|
||||
f"{r['third_party_loc']:>9,} lines third-party"
|
||||
)
|
||||
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
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue