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
"""scorecard.py — inject shields.io badges into README.md from a
captchaforge bench scorecard JSON.

Usage:
    python3 tools/scorecard.py --scorecard PATH_TO_SCORECARD_JSON [--readme README.md]

The scorecard JSON is the output of `cargo run --release -- --suite
real-waf --output scorecard` (see `bench/REAL_WAF_RUNBOOK.md`). The
script reads `.aggregates[]` from that file, builds one shields.io
badge per (suite, fixture) row, and replaces the content between
the `<!-- START-SCORECARD-BADGES -->` / `<!-- END-SCORECARD-BADGES -->`
markers in README.md.

When the markers don't exist in README.md, the script appends them
plus the badges to the end of the file.

Colors:
- success_rate >= 0.95 → brightgreen
- success_rate >= 0.80 → green
- success_rate >= 0.60 → yellow
- success_rate >= 0.40 → orange
- success_rate <  0.40 → red

Numbers are clamped to one decimal place.
"""

import argparse
import json
import re
import sys
import urllib.parse
from pathlib import Path


START_MARKER = "<!-- START-SCORECARD-BADGES -->"
END_MARKER = "<!-- END-SCORECARD-BADGES -->"


def color_for_rate(rate: float) -> str:
    if rate >= 0.95:
        return "brightgreen"
    if rate >= 0.80:
        return "green"
    if rate >= 0.60:
        return "yellow"
    if rate >= 0.40:
        return "orange"
    return "red"


def badge_url(label: str, value: str, color: str) -> str:
    enc_label = urllib.parse.quote(label, safe="")
    enc_value = urllib.parse.quote(value, safe="")
    return f"https://img.shields.io/badge/{enc_label}-{enc_value}-{color}"


def build_badges(scorecard: dict) -> list[str]:
    """Build one markdown image per (suite, fixture) aggregate row."""
    badges: list[str] = []
    for agg in scorecard.get("aggregates", []):
        suite = agg.get("suite", "")
        fixture = agg.get("fixture", "")
        rate = float(agg.get("success_rate", 0.0))
        if suite.startswith("_") or fixture.startswith("_"):
            continue  # availability rows etc.
        if suite != "real_waf" and suite != "accuracy" and suite != "solve":
            continue
        rate_pct = f"{rate * 100:.1f}%"
        label = f"{suite}_{fixture}".replace("/", "_").strip("_")
        badges.append(
            f"![{label}]({badge_url(label, rate_pct, color_for_rate(rate))})"
        )
    return badges


def inject_badges(readme: str, badges: list[str]) -> str:
    block = "\n".join(badges) if badges else "_(no scorecard rows yet)_"
    full_block = f"{START_MARKER}\n{block}\n{END_MARKER}"
    if START_MARKER in readme and END_MARKER in readme:
        return re.sub(
            re.escape(START_MARKER) + r"[\s\S]*?" + re.escape(END_MARKER),
            full_block,
            readme,
            count=1,
        )
    # No markers — append at end.
    sep = "\n\n" if not readme.endswith("\n\n") else ""
    return readme + sep + "## Scorecard badges\n\n" + full_block + "\n"


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--scorecard", required=True, help="Path to scorecard JSON")
    parser.add_argument("--readme", default="README.md", help="Path to README")
    args = parser.parse_args()

    scorecard_path = Path(args.scorecard)
    readme_path = Path(args.readme)
    if not scorecard_path.is_file():
        print(f"scorecard.py: scorecard JSON not found at {scorecard_path}", file=sys.stderr)
        return 2
    if not readme_path.is_file():
        print(f"scorecard.py: README not found at {readme_path}", file=sys.stderr)
        return 2

    scorecard = json.loads(scorecard_path.read_text())
    badges = build_badges(scorecard)
    original = readme_path.read_text()
    new = inject_badges(original, badges)
    if new == original:
        print("scorecard.py: no change")
        return 0
    readme_path.write_text(new)
    print(f"scorecard.py: wrote {len(badges)} badge(s) to {readme_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main())