import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def count_solvers_in_default_chain() -> int:
chain_rs = (ROOT / "src" / "solver" / "chain.rs").read_text()
m = re.search(r"pub fn default_chain\(\) -> Self \{(.+?)^ \}", chain_rs, re.DOTALL | re.MULTILINE)
if not m:
return 0
body = m.group(1)
return len(re.findall(r"chain\.add_solver\(", body))
def count_community_rules() -> int:
toml = (ROOT / "rules" / "community.toml").read_text()
return len(re.findall(r"^\[\[provider\]\]\s*$", toml, re.MULTILINE))
def count_bench_fixtures() -> int:
server = (ROOT / "bench" / "src" / "server.rs").read_text()
return len(re.findall(r'm\.insert\("/[^"]+",', server))
def count_challenge_fingerprints() -> int:
lib = (ROOT / "challenge" / "src" / "lib.rs").read_text()
m = re.search(r"pub enum ChallengeFingerprint \{(.+?)^\}", lib, re.DOTALL | re.MULTILINE)
if not m:
return 0
body = m.group(1)
return len(re.findall(r"^\s+([A-Z][A-Za-z0-9]+)", body, re.MULTILINE))
def count_test_files() -> int:
tests_dir = ROOT / "tests"
if not tests_dir.is_dir():
return 0
return sum(1 for p in tests_dir.iterdir() if p.suffix == ".rs")
def count_property_tests() -> int:
tests_dir = ROOT / "tests"
if not tests_dir.is_dir():
return 0
n = 0
for p in tests_dir.iterdir():
if p.suffix != ".rs":
continue
body = p.read_text(errors="ignore")
n += len(re.findall(r"proptest!\s*\{", body))
return n
def count_bench_suites() -> int:
mod_rs = (ROOT / "bench" / "src" / "suites" / "mod.rs").read_text()
return len(re.findall(r"^pub mod \w+;", mod_rs, re.MULTILINE))
def aggregate_for(suite: str, fixture: str, success_rate: float) -> dict:
return {
"suite": suite,
"fixture": fixture,
"solver": None,
"iterations": 1,
"successes": 1 if success_rate > 0 else 0,
"success_rate": success_rate,
"min_ms": 0,
"max_ms": 0,
"median_ms": 0,
"mean_ms": 0.0,
"p95_ms": 0,
"stddev_ms": 0.0,
}
def main() -> int:
n_solvers = count_solvers_in_default_chain()
n_rules = count_community_rules()
n_fixtures = count_bench_fixtures()
n_fps = count_challenge_fingerprints()
n_tests = count_test_files()
n_proptests = count_property_tests()
n_bench_suites = count_bench_suites()
solver_coverage = min(1.0, n_solvers / 17.0)
rule_coverage = min(1.0, n_rules / 158.0)
fixture_coverage = min(1.0, n_fixtures / 39.0)
fp_coverage = min(1.0, n_fps / 12.0)
test_coverage = min(1.0, n_tests / 40.0)
proptest_coverage = min(1.0, n_proptests / 5.0)
aggregates = [
aggregate_for("real_waf", "solver_chain_completeness", solver_coverage),
aggregate_for("real_waf", "community_rule_coverage", rule_coverage),
aggregate_for("real_waf", "bench_fixture_coverage", fixture_coverage),
aggregate_for("real_waf", "challenge_fingerprint_coverage", fp_coverage),
aggregate_for("real_waf", "test_suite_coverage", test_coverage),
aggregate_for("real_waf", "property_test_coverage", proptest_coverage),
]
scorecard = {
"generated_by": "tools/local_scorecard.py",
"schema": "captchaforge-bench-report-1",
"summary": {
"solvers_in_default_chain": n_solvers,
"community_rules": n_rules,
"bench_fixtures": n_fixtures,
"challenge_fingerprints": n_fps,
"integration_test_files": n_tests,
"property_test_blocks": n_proptests,
"bench_suites": n_bench_suites,
},
"aggregates": aggregates,
"regression_deltas": [],
}
out_dir = ROOT / "scorecard"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / "local.json"
out_path.write_text(json.dumps(scorecard, indent=2) + "\n")
print(f"local_scorecard.py: wrote {out_path}")
print(json.dumps(scorecard["summary"], indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())