captchaforge 0.2.39

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! Lock the training pipeline file set so future refactors can't
//! drop a script + leave RUNBOOK.md pointing at a missing file.

use std::path::PathBuf;

fn training_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("training")
}

#[test]
fn pipeline_scripts_all_present() {
    for script in [
        "scripts/auto_label.py",
        "scripts/train_lora.py",
        "scripts/distill.py",
        "scripts/bench_compare.py",
        "scripts/promote_to_ollama.sh",
    ] {
        let p = training_root().join(script);
        assert!(
            p.is_file(),
            "training pipeline script missing: {}",
            p.display()
        );
    }
}

#[test]
fn dockerfile_and_requirements_present() {
    assert!(training_root().join("Dockerfile").is_file());
    assert!(training_root().join("requirements.txt").is_file());
}

#[test]
fn runbook_references_existing_scripts_only() {
    let runbook = std::fs::read_to_string(training_root().join("RUNBOOK.md")).unwrap();
    // Every `training/scripts/<name>` mention in the runbook must
    // resolve to a real file. Plain-text scan to avoid pulling a
    // regex dep just for one test.
    const NEEDLE: &str = "training/scripts/";
    let mut missing: Vec<String> = Vec::new();
    let mut search_from = 0;
    while let Some(start) = runbook[search_from..].find(NEEDLE) {
        let abs_start = search_from + start;
        let rest = &runbook[abs_start..];
        // Take chars allowed in a script path.
        let end = rest
            .char_indices()
            .find(|(_, c)| !(c.is_ascii_alphanumeric() || matches!(c, '/' | '.' | '_' | '-')))
            .map(|(i, _)| i)
            .unwrap_or(rest.len());
        let mut rel = &rest[..end];
        // Trim trailing punctuation like `.` from a sentence end.
        while rel.ends_with('.') {
            rel = &rel[..rel.len() - 1];
        }
        let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(rel);
        if !p.is_file() && !p.is_dir() {
            missing.push(rel.to_string());
        }
        search_from = abs_start + end;
    }
    assert!(
        missing.is_empty(),
        "RUNBOOK references missing paths: {missing:?}"
    );
}