captchaforge 0.2.36

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for chromiumoxide-driven browsers. The architecture is in place (vendor solvers, retry-loop iframe walking, VLM provider abstraction, real-WAF bench harness) but the live-vendor success rate is still 0% — Cloudflare Turnstile / hCaptcha / reCAPTCHA detect us at a TLS / CDP fingerprint layer that no flag-based stealth has cleared. Watch the repo; do not depend on this for any real workload.
Documentation
#!/usr/bin/env python3
"""Generate a captchaforge LOCAL scorecard (no live WAFs).

Reads the deterministic state of the workspace:
- captchaforge solver chain composition (number of solvers in default_chain)
- bench fixture registry (count + per-vendor coverage)
- challenge crate ChallengeFingerprint variants
- community.toml rule count
- test-suite counts (estimated from cargo test --list)

Outputs scorecard/local.json — a structured snapshot of what
captchaforge DEMONSTRABLY does on this machine, with no live WAF
calls. Pairs with tools/scorecard.py to inject README badges
reflecting the local-bench numbers.

This is NOT the real-WAF scorecard (that comes from
.github/workflows/scorecard.yml against production endpoints).
This is the "what does the code structurally cover" snapshot —
gives operators a starting baseline without needing network keys.
"""

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:
    """Parse src/solver/chain.rs::default_chain to count
    `chain.add_solver(...)` invocations."""
    chain_rs = (ROOT / "src" / "solver" / "chain.rs").read_text()
    # Bound to the default_chain function body.
    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()
    # m.insert("/path", ...) lines.
    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)
    # variants: identifier followed by `,` or `{`
    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:
    """Build one aggregate row matching the captchaforge bench
    Observation shape so the same tools/scorecard.py consumes it."""
    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()

    # Coverage ratios scored 0..1 based on minimum bars.
    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())