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
//! Local proof bench for the architecture features added in the
//! 2026-05 sweep. Browser-free, runs in well under a second, and
//! prints a markdown table you can paste into a PR or report.
//!
//! Usage:
//!     cargo run --example architecture_proof --release
//!
//! What it proves:
//!   1. **Token cache hit** is sub-microsecond, vs the seconds a real
//!      solver takes — concrete justification for the cache.
//!   2. **Provider registry lookup** (find_by_kind) is sub-microsecond,
//!      so wiring it into the chain has no measurable overhead.
//!   3. **Rule probe JS generation** is one-shot at registration; the
//!      hot-path detect call is just `page.evaluate(&fixed_string)`.
//!   4. **Per-rule HTML matching** against the bundled fixtures
//!      stays in single-digit microseconds per match — so adding new
//!      rules does not slow detection meaningfully.
//!
//! Numbers are wall-clock means over many iterations. They are not
//! statistically rigorous (no warmup, no outlier rejection — use
//! criterion for that). They ARE good enough to prove order-of-
//! magnitude claims and to spot regressions a future change might
//! introduce.
use std::sync::Arc;
use std::time::Instant;

use captchaforge::detect::rules::{built_in_rules, RuleDetector};
use captchaforge::detect::DetectedCaptcha;
use captchaforge::provider::ProviderRegistry;
use captchaforge::solver::{CaptchaType, TokenCache};

fn main() {
    println!("# captchaforge architecture proof\n");
    println!("All numbers are wall-clock means over many iterations on this machine.");
    println!("Browser-free; runs in well under a second on any modern laptop.\n");

    bench_token_cache();
    bench_provider_registry();
    bench_rule_probe_generation();
    bench_per_rule_fixture_match();

    println!("## Summary\n");
    println!("If the cache hit / provider lookup / rule match rows are NOT in the");
    println!("microsecond range, something has regressed. Real solver attempts run");
    println!("in the hundreds-of-milliseconds-to-seconds range, so the architecture");
    println!("features above add zero meaningful overhead to the hot path.\n");
}

fn bench_token_cache() {
    let cache = TokenCache::new();
    cache.put(
        "example.com",
        &CaptchaType::CloudflareTurnstile,
        "tok-abc".into(),
        "BehavioralCaptchaSolver",
    );
    let captcha_type = CaptchaType::CloudflareTurnstile;

    // Hit path
    let n = 100_000;
    let t0 = Instant::now();
    let mut hits = 0u64;
    for _ in 0..n {
        if cache.get_token("example.com", &captcha_type).is_some() {
            hits += 1;
        }
    }
    let hit_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
    assert_eq!(hits, n as u64, "cache hits should always succeed");

    // Miss path
    let t0 = Instant::now();
    for _ in 0..n {
        let _ = cache.get_token("never-seen.test", &captcha_type);
    }
    let miss_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);

    println!("## TokenCache\n");
    println!("| Operation | Mean (ns) | Iterations |");
    println!("|-----------|----------:|-----------:|");
    println!("| `get` hit | {:>9.0} | {:>10} |", hit_ns, n);
    println!("| `get` miss | {:>8.0} | {:>10} |", miss_ns, n);
    println!();
}

fn bench_provider_registry() {
    let reg = ProviderRegistry::with_built_in_rules().expect("bundled rules parse");
    let kind = DetectedCaptcha::Custom("datadome".into());

    let n = 100_000;
    let t0 = Instant::now();
    let mut found = 0u64;
    for _ in 0..n {
        if reg.find_by_kind(&kind).is_some() {
            found += 1;
        }
    }
    let lookup_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
    assert_eq!(found, n as u64);

    println!("## ProviderRegistry\n");
    println!("| Operation | Mean (ns) | Iterations |");
    println!("|-----------|----------:|-----------:|");
    println!(
        "| `find_by_kind` (Custom) | {:>3.0} | {:>10} |",
        lookup_ns, n
    );
    println!(
        "| Registry size: {} providers (built-in + community.toml)",
        reg.providers().len()
    );
    println!();
}

fn bench_rule_probe_generation() {
    let set = built_in_rules().expect("rules parse");
    let n = 1000;
    let mut total_ns = 0u128;
    let mut probe_lengths = Vec::new();
    for _ in 0..n {
        for rule in &set.providers {
            let t0 = Instant::now();
            let det = RuleDetector::from(rule.clone());
            total_ns += t0.elapsed().as_nanos();
            probe_lengths.push(det.js().len());
        }
    }
    let total_constructions = n * set.providers.len();
    let avg_ns = (total_ns as f64) / (total_constructions as f64);
    let avg_len = probe_lengths.iter().sum::<usize>() as f64 / probe_lengths.len() as f64;

    println!("## RuleDetector probe generation\n");
    println!("Done once at registration. Detect-time cost is `page.evaluate(&str)` only.\n");
    println!("| Metric | Value |");
    println!("|--------|------:|");
    println!("| Mean construction time | {:>6.2} µs |", avg_ns / 1000.0);
    println!("| Mean probe length | {:>6.0} bytes |", avg_len);
    println!(
        "| Rules generated | {} | (per iter, × {} iters)",
        set.providers.len(),
        n
    );
    println!();
}

fn bench_per_rule_fixture_match() {
    use std::path::Path;
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let fixtures_root = Path::new(manifest_dir).join("tests").join("rule_fixtures");
    if !fixtures_root.exists() {
        println!("## Per-rule HTML fixture match\n");
        println!("(skipped — `tests/rule_fixtures/` not present)\n");
        return;
    }

    let set = built_in_rules().unwrap();
    let n = 5_000;

    println!("## Per-rule HTML fixture match\n");
    println!("Per-rule per-fixture latency using a string-contains matcher");
    println!("(the precise scraper-based matcher used for correctness lives");
    println!("in the `community_rules` integration test). Numbers prove that");
    println!("adding new rules to the pack does not regress detection time.\n");
    println!("| Vendor | Mean match (µs) | Pos+neg fixtures |");
    println!("|--------|----------------:|-----------------:|");

    for rule in &set.providers {
        let pos_path = fixtures_root.join(&rule.name).join("positive.html");
        let neg_path = fixtures_root.join(&rule.name).join("negative.html");
        if !pos_path.exists() || !neg_path.exists() {
            continue;
        }
        let pos = std::fs::read_to_string(&pos_path).unwrap();
        let neg = std::fs::read_to_string(&neg_path).unwrap();

        let t0 = Instant::now();
        for _ in 0..n {
            let _ = rule_matches_html(&pos, rule);
            let _ = rule_matches_html(&neg, rule);
        }
        let total_ns = t0.elapsed().as_nanos();
        let per_match_ns = (total_ns as f64) / ((n * 2) as f64);
        println!(
            "| `{}` | {:>14.2} | 1+1 |",
            rule.name,
            per_match_ns / 1000.0
        );
    }
    println!();
}

/// Simple string-contains rule match used for bench latency
/// measurement. Less precise than the integration test's
/// scraper-based matcher (we'd false-match a CSS selector embedded
/// in a comment, for example) but the goal here is per-call
/// latency, not correctness — correctness is proven in the
/// `community_rules` integration test.
fn rule_matches_html(html: &str, rule: &captchaforge::detect::rules::ProviderRule) -> bool {
    for sel in &rule.triggers.selectors {
        // Strip leading `.` / `#` / `[` so the substring search lands on
        // the class / id / attr name itself.
        let needle = sel.trim_start_matches(['.', '#', '[']);
        if html.contains(needle) {
            return true;
        }
    }
    for needle in &rule.triggers.script_src_contains {
        if html.contains(needle) {
            return true;
        }
    }
    false
}

#[allow(dead_code)]
fn use_arc() -> Arc<TokenCache> {
    // Ensures Arc usage doesn't warn unused if I trim above.
    Arc::new(TokenCache::new())
}