//! Index command-line prototype.

#[cfg(test)]
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::io::{self, Read};
use std::mem;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

mod compat_pack_runtime;

use compat_pack_runtime::{
    PackSource, inspect_runtime_for_url, install_pack_file, lint_pack_file,
    list_runtime_pack_files, rollback_pack_file, select_runtime_manifest_for_url, sign_pack_bytes,
    verify_pack_signature,
};
use image::imageops::{BiLevel, FilterType, dither, resize};
use index_ai::{AiAction, OfflineProvider, PrivacyMode, run_ai_action};
use index_capture::{
    ArtifactContext, ArtifactFreshness, ArtifactStore, CaptureArtifact, CaptureRequest,
    IndexArtifact, capture_redacted, catalog_entry_for_fixture, preview_redacted,
    validate_capture_bundle,
};
use index_core::{CookieJar, MemorySecureStorage, Redactor, SecureStorage};
use index_core::{
    DiagnosticAction, DiagnosticConfidence, DiagnosticRecord, DiagnosticSeverity, DiagnosticSource,
    FailureDiagnostic, FormSubmission, IndexDocument, IndexNode, IndexUrl, KnowledgeShelf, Link,
    ReaderProfile, ResponseLogEntry, ShelfRecord, ShelfSearchResult,
};
use index_dom::{
    IndexManifest, IndexManifestError, discover_index_manifest_link_from_html,
    discover_index_manifest_link_from_http_link_header, parse_index_manifest,
    well_known_index_manifest_url,
};
use index_extract::{
    ExtractFormat, ExtractionLimits, export_section_markdown, extract_citations_tsv,
    extract_document, try_extract_document,
};
use index_http::{Fetcher, FormSubmitter, Request, RetryingFetcher, SecureFetcher, UreqFetcher};
use index_renderer::{
    RenderOptions, TuiDocumentResult, render_document,
    run_tui_with_navigation_profile_forms_and_state_with_progress,
};
use index_security::{ContentLimits, check_content_size};
use index_transformer::{
    TransformedDocumentCache, Transformer, apply_index_manifest_hints, state::Empty,
    transform_html_cached,
};

const VERSION: &str = env!("CARGO_PKG_VERSION");
const IMAGE_PREVIEW_MAX_IMAGES_PER_DOCUMENT: usize = 8;
const IMAGE_PREVIEW_MAX_FETCH_BYTES: usize = 1024 * 1024;
const IMAGE_PREVIEW_MAX_WIDTH: u32 = 101;
const IMAGE_PREVIEW_MAX_HEIGHT: u32 = 67;
const IMAGE_PREVIEW_TIMEOUT: Duration = Duration::from_secs(4);
const ARTIFACT_MAX_AGE_SECS: u64 = 900;
const STAGE_FETCH_TIMEOUT: Duration = Duration::from_secs(120);
const STAGE_SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(30);
const STAGE_PARSE_TIMEOUT: Duration = Duration::from_secs(15);
const STAGE_TRANSFORM_TIMEOUT: Duration = Duration::from_secs(120);
const STAGE_SCORE_TIMEOUT: Duration = Duration::from_secs(10);
const STAGE_STORE_TIMEOUT: Duration = Duration::from_secs(10);
const COMPATIBILITY_SLO_TOP100_PATH: &str = "docs/top100-corpus/matrix.tsv";
const COMPATIBILITY_SLO_FORUM_PATH: &str = "docs/forum-corpus/matrix.tsv";
const LIVE_VARIANCE_TARGETS_PATH: &str = "docs/live-variance/targets.tsv";
const LIVE_VARIANCE_RUNS_PATH: &str = "docs/live-variance/runs.tsv";
const COMPATIBILITY_SLO_READABILITY_FLOOR: f64 = 0.85;
const COMPATIBILITY_SLO_ACTIONABILITY_FLOOR: f64 = 0.40;
const COMPATIBILITY_SLO_FAILURE_QUALITY_FLOOR: f64 = 0.98;
const COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_READABILITY_FLOOR: f64 = 0.35;
const COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_ACTIONABILITY_FLOOR: f64 = 0.15;
const COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_FAILURE_FLOOR: f64 = 0.98;
const COMPATIBILITY_BACKLOG_TOP_N_DEFAULT: usize = 20;
const LIVE_VARIANCE_WINDOW_DEFAULT: usize = 10;
const LIVE_VARIANCE_MAX_WINDOW: usize = 120;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RuntimeStage {
    Queued,
    Fetching,
    Snapshotting,
    Parsing,
    Transforming,
    Scoring,
    Storing,
    Done,
    Failed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RecoveryProfile {
    Default,
    AppShell,
}

impl RecoveryProfile {
    const fn as_str(self) -> &'static str {
        match self {
            Self::Default => "default",
            Self::AppShell => "app-shell",
        }
    }
}

impl RuntimeStage {
    const fn as_str(self) -> &'static str {
        match self {
            Self::Queued => "queued",
            Self::Fetching => "fetching",
            Self::Snapshotting => "snapshotting",
            Self::Parsing => "parsing",
            Self::Transforming => "transforming",
            Self::Scoring => "scoring",
            Self::Storing => "storing",
            Self::Done => "done",
            Self::Failed => "failed",
        }
    }

    const fn default_timeout(self) -> Option<Duration> {
        match self {
            Self::Fetching => Some(STAGE_FETCH_TIMEOUT),
            Self::Snapshotting => Some(STAGE_SNAPSHOT_TIMEOUT),
            Self::Parsing => Some(STAGE_PARSE_TIMEOUT),
            Self::Transforming => Some(STAGE_TRANSFORM_TIMEOUT),
            Self::Scoring => Some(STAGE_SCORE_TIMEOUT),
            Self::Storing => Some(STAGE_STORE_TIMEOUT),
            Self::Queued | Self::Done | Self::Failed => None,
        }
    }
}

fn recovery_profile_for_target(target: &str) -> RecoveryProfile {
    let normalized = normalize_url_input(target);
    let parsed = url::Url::parse(&normalized);
    let host = parsed
        .ok()
        .and_then(|url| url.host_str().map(str::to_ascii_lowercase))
        .unwrap_or_default();
    if matches!(
        host.as_str(),
        "chatgpt.com"
            | "claude.ai"
            | "gemini.google.com"
            | "deepseek.com"
            | "chat.deepseek.com"
            | "discord.com"
            | "facebook.com"
            | "instagram.com"
            | "linkedin.com"
            | "netflix.com"
            | "roblox.com"
            | "tiktok.com"
            | "douyin.com"
            | "x.com"
            | "youtube.com"
            | "music.youtube.com"
    ) {
        return RecoveryProfile::AppShell;
    }
    RecoveryProfile::Default
}

fn stage_timeout_for_target(stage: RuntimeStage, target: &str) -> Option<Duration> {
    let profile = recovery_profile_for_target(target);
    match (profile, stage) {
        (RecoveryProfile::AppShell, RuntimeStage::Fetching) => Some(Duration::from_secs(180)),
        (RecoveryProfile::AppShell, RuntimeStage::Snapshotting) => Some(Duration::from_secs(45)),
        (RecoveryProfile::AppShell, RuntimeStage::Transforming) => Some(Duration::from_secs(180)),
        (RecoveryProfile::AppShell, RuntimeStage::Scoring) => Some(Duration::from_secs(15)),
        (RecoveryProfile::AppShell, RuntimeStage::Storing) => Some(Duration::from_secs(15)),
        _ => stage.default_timeout(),
    }
}

fn recovery_fallback_order(profile: RecoveryProfile) -> &'static [&'static str] {
    match profile {
        RecoveryProfile::Default => &["static-dom", "accessibility-snapshot", "rendered-dom"],
        RecoveryProfile::AppShell => &["accessibility-snapshot", "rendered-dom", "static-dom"],
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct StageFlow {
    last: Option<RuntimeStage>,
}

impl StageFlow {
    fn advance(&mut self, next: RuntimeStage) -> Result<(), String> {
        if !is_valid_stage_transition(self.last, next) {
            return Err(format!(
                "invalid stage transition: {:?} -> {:?}",
                self.last, next
            ));
        }
        self.last = Some(next);
        Ok(())
    }
}

fn is_valid_stage_transition(previous: Option<RuntimeStage>, next: RuntimeStage) -> bool {
    matches!(
        (previous, next),
        (None, RuntimeStage::Queued)
            | (Some(RuntimeStage::Queued), RuntimeStage::Fetching)
            | (Some(RuntimeStage::Queued), RuntimeStage::Snapshotting)
            | (Some(RuntimeStage::Queued), RuntimeStage::Parsing)
            | (Some(RuntimeStage::Queued), RuntimeStage::Transforming)
            | (Some(RuntimeStage::Queued), RuntimeStage::Failed)
            | (Some(RuntimeStage::Fetching), RuntimeStage::Snapshotting)
            | (Some(RuntimeStage::Fetching), RuntimeStage::Parsing)
            | (Some(RuntimeStage::Fetching), RuntimeStage::Failed)
            | (Some(RuntimeStage::Snapshotting), RuntimeStage::Parsing)
            | (Some(RuntimeStage::Snapshotting), RuntimeStage::Failed)
            | (Some(RuntimeStage::Parsing), RuntimeStage::Transforming)
            | (Some(RuntimeStage::Parsing), RuntimeStage::Failed)
            | (Some(RuntimeStage::Transforming), RuntimeStage::Scoring)
            | (Some(RuntimeStage::Transforming), RuntimeStage::Failed)
            | (Some(RuntimeStage::Scoring), RuntimeStage::Storing)
            | (Some(RuntimeStage::Scoring), RuntimeStage::Done)
            | (Some(RuntimeStage::Scoring), RuntimeStage::Failed)
            | (Some(RuntimeStage::Storing), RuntimeStage::Done)
            | (Some(RuntimeStage::Storing), RuntimeStage::Failed)
    )
}

fn report_stage(
    flow: &mut StageFlow,
    stage: RuntimeStage,
    target: &str,
    report_progress: &mut dyn FnMut(String),
) -> Result<(), String> {
    flow.advance(stage)?;
    report_progress(format!("{} {target}", stage.as_str()));
    Ok(())
}

fn enforce_stage_budget(
    stage: RuntimeStage,
    elapsed: Duration,
    target: &str,
) -> Result<(), String> {
    let Some(timeout) = stage_timeout_for_target(stage, target) else {
        return Ok(());
    };
    if elapsed > timeout {
        return Err(format!(
            "stage {} timed out after {:?} for {target}",
            stage.as_str(),
            elapsed
        ));
    }
    Ok(())
}

fn candidate_manifest_urls(
    page_url: &str,
    html: &str,
    http_link_header: Option<&str>,
) -> Vec<String> {
    let mut candidates = Vec::new();
    if let Some(url) = well_known_index_manifest_url(page_url) {
        candidates.push(url);
    }
    if let Some(link_header) = http_link_header {
        if let Some(url) = discover_index_manifest_link_from_http_link_header(link_header, page_url)
        {
            if !candidates.contains(&url) {
                candidates.push(url);
            }
        }
    }
    if let Some(url) = discover_index_manifest_link_from_html(html, page_url) {
        if !candidates.contains(&url) {
            candidates.push(url);
        }
    }
    candidates
}

fn load_index_manifest_for_response<F: Fetcher>(
    fetcher: &F,
    response: &index_http::Response,
) -> Option<IndexManifest> {
    let page_url = response.final_url.as_str();
    let candidates = candidate_manifest_urls(page_url, &response.body, None);
    for candidate in candidates {
        let url = match IndexUrl::parse(&candidate) {
            Ok(url) => url,
            Err(_) => continue,
        };
        let manifest_response = match fetcher.fetch(&Request { url }) {
            Ok(response) => response,
            Err(_) => continue,
        };
        let parsed = parse_index_manifest(
            &manifest_response.body,
            manifest_response.final_url.as_str(),
            page_url,
        );
        if let Ok(manifest) = parsed {
            return Some(manifest);
        }
    }
    None
}

fn apply_runtime_pack_manifest_hints(document: &mut IndexDocument, final_url: &str) {
    let paths = runtime_paths();
    if let Ok(Some(selected)) = select_runtime_manifest_for_url(&paths.config_dir, final_url) {
        apply_index_manifest_hints(document, &selected.manifest);
        if let Some(quality) = document.metadata.quality.as_mut() {
            quality.reasons.push(format!(
                "compatibility pack {}:{} matched {}{}",
                selected.source.as_str(),
                selected.pack_id,
                selected.rule_host,
                selected.rule_path_prefix
            ));
        }
    }
}

fn main() -> ExitCode {
    match run() {
        Ok(CliAction::Print(output)) => {
            println!("{output}");
            ExitCode::SUCCESS
        }
        Ok(CliAction::Tui {
            document,
            profile,
            url_history,
        }) => {
            let fetcher = RetryingFetcher::new(SecureFetcher::new(UreqFetcher::new()));
            let transform_cache = Arc::new(Mutex::new(TransformedDocumentCache::new()));
            let response_log_sequence = Arc::new(Mutex::new(0_u64));
            let artifact_store = Arc::new(ArtifactStore::new(artifact_store_root()));
            match run_tui_with_navigation_profile_forms_and_state_with_progress(
                *document,
                profile,
                url_history,
                Vec::new(),
                {
                    let fetcher = fetcher.clone();
                    let transform_cache = Arc::clone(&transform_cache);
                    let response_log_sequence = Arc::clone(&response_log_sequence);
                    let artifact_store = Arc::clone(&artifact_store);
                    move |target, report_progress| {
                        let mut cache = transform_cache
                            .lock()
                            .map_err(|_| "transform cache lock poisoned".to_owned())?;
                        let mut sequence = response_log_sequence
                            .lock()
                            .map_err(|_| "response log lock poisoned".to_owned())?;
                        fetch_document_with_artifact_runtime(
                            &fetcher,
                            &mut cache,
                            &mut sequence,
                            &artifact_store,
                            target,
                            report_progress,
                        )
                    }
                },
                {
                    let fetcher = fetcher.clone();
                    let transform_cache = Arc::clone(&transform_cache);
                    let response_log_sequence = Arc::clone(&response_log_sequence);
                    let artifact_store = Arc::clone(&artifact_store);
                    move |submission, report_progress| {
                        let mut cache = transform_cache
                            .lock()
                            .map_err(|_| "transform cache lock poisoned".to_owned())?;
                        let mut sequence = response_log_sequence
                            .lock()
                            .map_err(|_| "response log lock poisoned".to_owned())?;
                        submit_form_with_artifact_runtime(
                            &fetcher,
                            &mut cache,
                            &mut sequence,
                            &artifact_store,
                            submission,
                            report_progress,
                        )
                    }
                },
            ) {
                Ok(()) => ExitCode::SUCCESS,
                Err(error) => {
                    eprintln!("index: failed to run terminal UI: {error}");
                    ExitCode::from(1)
                }
            }
        }
        Ok(CliAction::Help(output)) => {
            println!("{output}");
            ExitCode::SUCCESS
        }
        Ok(CliAction::Version(output)) => {
            println!("{output}");
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("index: {error}");
            ExitCode::from(1)
        }
    }
}

fn run() -> Result<CliAction, String> {
    let args = env::args().skip(1).collect::<Vec<_>>();
    let mut stdin = io::stdin();
    run_with_args(args, &mut stdin)
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum CliAction {
    Help(String),
    Print(String),
    Tui {
        document: Box<IndexDocument>,
        profile: ReaderProfile,
        url_history: Vec<String>,
    },
    Version(String),
}

fn run_with_args<I, S>(args: I, stdin: &mut dyn Read) -> Result<CliAction, String>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let fetcher = RetryingFetcher::new(SecureFetcher::new(UreqFetcher::new()));
    run_with_args_and_fetcher(args, stdin, &fetcher)
}

fn run_with_args_and_fetcher<I, S, F>(
    args: I,
    stdin: &mut dyn Read,
    fetcher: &F,
) -> Result<CliAction, String>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
    F: Fetcher,
{
    let mut args = args.into_iter();
    let mut output_mode = OutputMode::Tui;
    let mut profile = ReaderProfile::Reader;
    let Some(input) = args.next() else {
        return Ok(CliAction::Tui {
            document: Box::new(default_start_page()),
            profile,
            url_history: Vec::new(),
        });
    };

    let mut input = input.as_ref().to_owned();
    if input == "--profile" {
        let Some(next_profile) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        profile = ReaderProfile::parse(next_profile.as_ref())
            .ok_or_else(|| format!("unsupported reader profile: {}", next_profile.as_ref()))?;
        let Some(next_input) = args.next() else {
            return Ok(CliAction::Tui {
                document: Box::new(default_start_page()),
                profile,
                url_history: Vec::new(),
            });
        };
        input = next_input.as_ref().to_owned();
    }

    if input == "--help" || input == "-h" {
        return Ok(CliAction::Help(help()));
    }
    if input == "--version" || input == "-V" {
        return Ok(CliAction::Version(version()));
    }
    if input == "--paths" {
        return Ok(CliAction::Print(runtime_paths().display()));
    }
    if input == "doctor" {
        if args.next().is_some() {
            return Ok(CliAction::Help(help()));
        }
        return Ok(CliAction::Print(doctor_report(&runtime_paths())));
    }
    if input == "quickstart" {
        if args.next().is_some() {
            return Ok(CliAction::Help(help()));
        }
        return Ok(CliAction::Print(quickstart()));
    }
    if input == "compatibility-slo" {
        return run_compatibility_slo_command(args);
    }
    if input == "compatibility-slo-v2" {
        return run_compatibility_slo_v2_command(args);
    }
    if input == "compatibility-backlog" {
        return run_compatibility_backlog_command(args);
    }
    if input == "compatibility-live-variance" {
        return run_compatibility_live_variance_command(args);
    }
    if input == "compatibility-recovery-plan" {
        return run_compatibility_recovery_plan_command(args);
    }
    if input == "compatibility-recovery-gate" {
        return run_compatibility_recovery_gate_command(args);
    }
    if input == "compatibility-pack" {
        return run_compatibility_pack_command(args);
    }
    if input == "auth-assist" {
        return run_auth_assist_command(args);
    }
    if input == "challenge-diagnose" {
        return run_challenge_diagnose_command(args);
    }
    if input == "idx" {
        return run_idx_command(args);
    }
    if input == "--benchmark" {
        let Some(target) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        if args.next().is_some() {
            return Err("too many arguments".to_owned());
        }
        return run_benchmark_command(target.as_ref(), stdin, fetcher);
    }
    if input == "--save" {
        let Some(format) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        let Some(format) = ExtractFormat::parse(format.as_ref()) else {
            return Err(format!("unsupported save format: {}", format.as_ref()));
        };
        let Some(target) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        let Some(output_path) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        if args.next().is_some() {
            return Err("too many arguments".to_owned());
        }
        return run_save_command(
            format,
            target.as_ref(),
            output_path.as_ref(),
            stdin,
            fetcher,
        );
    }
    if input == "--citations" {
        let Some(target) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        if args.next().is_some() {
            return Err("too many arguments".to_owned());
        }
        let document = load_document(target.as_ref(), stdin, fetcher)?;
        return Ok(CliAction::Print(extract_citations_tsv(&document)));
    }
    if input == "--section" {
        let Some(selector) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        let Some(target) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        if args.next().is_some() {
            return Err("too many arguments".to_owned());
        }
        let document = load_document(target.as_ref(), stdin, fetcher)?;
        let section = export_section_markdown(&document, selector.as_ref())
            .ok_or_else(|| format!("section not found: {}", selector.as_ref()))?;
        return Ok(CliAction::Print(section));
    }
    if input == "--batch-extract" {
        let Some(format) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        let Some(format) = ExtractFormat::parse(format.as_ref()) else {
            return Err(format!(
                "unsupported batch extraction format: {}",
                format.as_ref()
            ));
        };
        let targets = args.map(|arg| arg.as_ref().to_owned()).collect::<Vec<_>>();
        if targets.is_empty() {
            return Ok(CliAction::Help(help()));
        }
        return run_batch_extract_command(format, &targets);
    }
    if input == "adapter" {
        return run_adapter_command(args);
    }
    if input == "artifact" {
        return run_artifact_command(args);
    }
    if input == "capture" {
        return run_capture_command(args, stdin);
    }
    if input == "shelf" {
        return run_shelf_command(args, stdin, fetcher);
    }
    if input == "--plain" {
        output_mode = OutputMode::Plain;
        let Some(next_input) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        input = next_input.as_ref().to_owned();
    } else if input == "--extract" {
        let Some(format) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        let Some(format) = ExtractFormat::parse(format.as_ref()) else {
            return Err(format!(
                "unsupported extraction format: {}",
                format.as_ref()
            ));
        };
        output_mode = OutputMode::Extract(format);
        let Some(next_input) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        input = next_input.as_ref().to_owned();
    } else if input == "--ai-offline" {
        let Some(action) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        let Some(action) = AiAction::parse(action.as_ref()) else {
            return Err(format!("unsupported AI action: {}", action.as_ref()));
        };
        output_mode = OutputMode::AiOffline(action);
        let Some(next_input) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        input = next_input.as_ref().to_owned();
    }

    if args.next().is_some() {
        return Err("too many arguments".to_owned());
    }

    let document = load_document(&input, stdin, fetcher)?;
    let url_history = should_fetch_input(&input).then(|| normalize_url_input(&input));

    match output_mode {
        OutputMode::Tui => Ok(CliAction::Tui {
            document: Box::new(document),
            profile,
            url_history: url_history.into_iter().collect(),
        }),
        OutputMode::Plain => Ok(CliAction::Print(render_document(
            &document,
            RenderOptions::default(),
        ))),
        OutputMode::Extract(format) => Ok(CliAction::Print(
            try_extract_document(&document, format, ExtractionLimits::default())
                .map_err(|error| error.to_string())?,
        )),
        OutputMode::AiOffline(action) => {
            let response = run_ai_action(
                &OfflineProvider,
                &document,
                action,
                PrivacyMode::Redacted,
                &Redactor::new(),
            )
            .map_err(|error| error.to_string())?;
            Ok(CliAction::Print(response.text))
        }
    }
}

fn run_capture_command<I, S>(mut args: I, stdin: &mut dyn Read) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(flag) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    if flag.as_ref() == "--validate" {
        let Some(input) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        if args.next().is_some() {
            return Err("too many arguments".to_owned());
        }
        let artifact_text = read_input(input.as_ref(), stdin)?;
        let artifact =
            validate_capture_bundle(&artifact_text).map_err(|error| error.to_string())?;
        return Ok(CliAction::Print(format!(
            "index-capture-validation-v1\nsource_url: {}\nstatus: ok\n{}",
            artifact.source_url,
            artifact.submission_checklist()
        )));
    }
    if flag.as_ref() == "--catalog-entry" {
        let Some(path) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        if args.next().is_some() {
            return Err("too many arguments".to_owned());
        }
        return Ok(CliAction::Print(
            catalog_entry_for_fixture(path.as_ref(), "unknown", 0)
                .map_err(|error| error.to_string())?,
        ));
    }

    let preview = if flag.as_ref() == "--preview" {
        let Some(redact_flag) = args.next() else {
            return Ok(CliAction::Help(help()));
        };
        if redact_flag.as_ref() != "--redact" {
            return Err(format!(
                "unsupported capture option: {}",
                redact_flag.as_ref()
            ));
        }
        true
    } else if flag.as_ref() == "--redact" {
        false
    } else {
        return Err(format!("unsupported capture option: {}", flag.as_ref()));
    };

    let Some(source_url) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    let Some(input) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    if args.next().is_some() {
        return Err("too many arguments".to_owned());
    }

    let html = read_input(input.as_ref(), stdin)?;
    let source_url = normalize_url_input(source_url.as_ref());
    let request = CaptureRequest::new(&source_url, html).map_err(|error| error.to_string())?;
    if preview {
        Ok(CliAction::Print(preview_redacted(&request).to_text()))
    } else {
        Ok(CliAction::Print(capture_redacted(&request).to_text()))
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputMode {
    Tui,
    Plain,
    Extract(ExtractFormat),
    AiOffline(AiAction),
}

fn read_input(input: &str, stdin: &mut dyn Read) -> Result<String, String> {
    read_input_with_limits(input, stdin, ContentLimits::default())
}

fn default_start_page() -> IndexDocument {
    let mut document = IndexDocument::titled("Index");
    document.push(IndexNode::Heading {
        level: 2,
        text: "Start".to_owned(),
    });
    document.push(IndexNode::Paragraph(
        "Open a page with :open <url>, or start Index with index example.org. Missing URL schemes default to HTTPS."
            .to_owned(),
    ));
    document.push(IndexNode::Paragraph(
        "Use f to show link hints, / to search, j/k to scroll, and :quit to leave.".to_owned(),
    ));
    document.push(IndexNode::Heading {
        level: 2,
        text: "Useful Commands".to_owned(),
    });
    document.push(IndexNode::List {
        ordered: false,
        items: vec![
            ":open <id-or-url> fetch and render a page".to_owned(),
            "e edit form fields; tab moves fields; enter submits".to_owned(),
            ":extract markdown|links|json request scriptable output".to_owned(),
            ":submit <form> field=value resolve a semantic form action".to_owned(),
            ":pipe <cmd> request a confirmed local pipe action".to_owned(),
            ":ai explain|summarize|extract request an explicit local AI action".to_owned(),
        ],
    });
    document.push(IndexNode::Heading {
        level: 2,
        text: "Local Inputs".to_owned(),
    });
    document.push(IndexNode::Paragraph(
        "Index also accepts local HTML files and stdin: index examples/sample.html or index - < file.html."
            .to_owned(),
    ));
    document.push(IndexNode::Paragraph(
        "Need the command cheat sheet? Run index quickstart.".to_owned(),
    ));
    document.push(IndexNode::Link(Link::new(
        "Example Domain",
        "https://example.org",
    )));
    document
}

fn load_document<F: Fetcher>(
    input: &str,
    stdin: &mut dyn Read,
    fetcher: &F,
) -> Result<IndexDocument, String> {
    if should_fetch_input(input) {
        let normalized = normalize_url_input(input);
        return match fetch_document(fetcher, &normalized) {
            Ok(document) => Ok(document),
            Err(error) => Ok(network_failure_document(&normalized, &error)),
        };
    }

    let html = read_input(input, stdin)?;
    Ok(transform_html(html))
}

fn network_failure_document(target: &str, error: &str) -> IndexDocument {
    FailureDiagnostic::new(
        "Network fetch failed",
        DiagnosticSource::Network,
        DiagnosticConfidence::Failed,
        format!("could not fetch {target}: {error}"),
    )
    .with_fallback("no document was transformed")
    .with_tried("URL normalization")
    .with_tried("secure fetcher")
    .with_tried("bounded retry policy")
    .with_actions([
        DiagnosticAction::Retry,
        DiagnosticAction::Extract,
        DiagnosticAction::Capture,
    ])
    .with_command(format!(":open {target}"))
    .with_command(":capture save network-failure.capture")
    .with_record(
        DiagnosticRecord::new(
            DiagnosticSeverity::Error,
            "INDEX-NETWORK-FAILED",
            error.to_owned(),
        )
        .with_field("target", target),
    )
    .into_document()
}

fn fetch_document<F: Fetcher>(fetcher: &F, target: &str) -> Result<IndexDocument, String> {
    let normalized = normalize_url_input(target);
    let url = IndexUrl::parse(&normalized).map_err(|error| error.to_string())?;
    if artifact_runtime_enabled() {
        let artifact_store = ArtifactStore::new(artifact_store_root());
        if let Some(artifact) = artifact_store
            .load(&url, ArtifactContext::LiveGet)
            .map_err(|error| error.to_string())?
        {
            if artifact.is_fresh(unix_timestamp_secs()) {
                return document_from_index_artifact_uncached(&artifact);
            }
        }
        let response = fetcher
            .fetch(&Request { url: url.clone() })
            .map_err(|error| error.to_string())?;
        let mut document = transform_html(html_with_base(&response.final_url, &response.body));
        document = hydrate_document_images(document);
        apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
        if let Ok(artifact) = IndexArtifact::from_document(
            &document,
            &url,
            &response.final_url,
            ArtifactContext::LiveGet,
            unix_timestamp_secs(),
            ARTIFACT_MAX_AGE_SECS,
        ) {
            let _ = artifact_store.store(&artifact);
        }
        return Ok(document);
    }

    let response = fetcher
        .fetch(&Request { url: url.clone() })
        .map_err(|error| error.to_string())?;
    let mut document = transform_html(html_with_base(&response.final_url, &response.body));
    document = hydrate_document_images(document);
    apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
    Ok(document)
}

#[cfg(test)]
fn fetch_document_cached_with_log<F: Fetcher>(
    fetcher: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &RefCell<u64>,
    target: &str,
) -> Result<TuiDocumentResult, String> {
    let mut next_sequence = sequence.borrow_mut();
    fetch_document_cached_with_log_with_counter(fetcher, cache, &mut next_sequence, target)
}

#[cfg(test)]
fn fetch_document_cached_with_log_with_counter<F: Fetcher>(
    fetcher: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &mut u64,
    target: &str,
) -> Result<TuiDocumentResult, String> {
    let mut ignore_progress = |_message: String| {};
    fetch_document_cached_with_log_with_counter_with_progress(
        fetcher,
        cache,
        sequence,
        target,
        &mut ignore_progress,
    )
}

fn fetch_document_with_artifact_runtime<F: Fetcher + Clone + Send + 'static>(
    fetcher: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &mut u64,
    artifact_store: &ArtifactStore,
    target: &str,
    report_progress: &mut dyn FnMut(String),
) -> Result<TuiDocumentResult, String> {
    let normalized = normalize_url_input(target);
    let canonical = IndexUrl::parse(&normalized).map_err(|error| error.to_string())?;
    let mut flow = StageFlow::default();
    report_stage(
        &mut flow,
        RuntimeStage::Queued,
        &normalized,
        report_progress,
    )?;
    if let Some(artifact) = artifact_store
        .load(&canonical, ArtifactContext::LiveGet)
        .map_err(|error| error.to_string())?
    {
        report_stage(
            &mut flow,
            RuntimeStage::Transforming,
            &artifact.final_url,
            report_progress,
        )?;
        let transform_started = Instant::now();
        let document = document_from_index_artifact(cache, &artifact).inspect_err(|_error| {
            let _ = report_stage(
                &mut flow,
                RuntimeStage::Failed,
                &normalized,
                report_progress,
            );
        })?;
        enforce_stage_budget(
            RuntimeStage::Transforming,
            transform_started.elapsed(),
            &artifact.final_url,
        )?;
        report_stage(
            &mut flow,
            RuntimeStage::Scoring,
            &artifact.final_url,
            report_progress,
        )?;
        let log = response_log_entry(
            sequence,
            "ARTIFACT",
            &artifact.canonical_url,
            &artifact.final_url,
            Some("index/artifact+v1"),
            &artifact.capture.redacted_html,
        );
        report_stage(
            &mut flow,
            RuntimeStage::Done,
            &artifact.final_url,
            report_progress,
        )?;
        let freshness = artifact.freshness(unix_timestamp_secs());
        if matches!(freshness, ArtifactFreshness::Stale) {
            let refresh_fetcher = fetcher.clone();
            let refresh_store = artifact_store.clone();
            let refresh_canonical = canonical.clone();
            std::thread::spawn(move || {
                let _ =
                    refresh_live_get_artifact(&refresh_fetcher, &refresh_store, &refresh_canonical);
            });
        }
        return Ok(TuiDocumentResult::new(document)
            .with_visited_url(artifact.final_url)
            .with_response_log(log));
    }
    report_stage(
        &mut flow,
        RuntimeStage::Fetching,
        &normalized,
        report_progress,
    )?;
    let fetch_started = Instant::now();
    let response = fetcher
        .fetch(&Request {
            url: canonical.clone(),
        })
        .map_err(|error| {
            let _ = report_stage(
                &mut flow,
                RuntimeStage::Failed,
                &normalized,
                report_progress,
            );
            error.to_string()
        })?;
    enforce_stage_budget(
        RuntimeStage::Fetching,
        fetch_started.elapsed(),
        response.final_url.as_str(),
    )?;

    report_stage(
        &mut flow,
        RuntimeStage::Snapshotting,
        response.final_url.as_str(),
        report_progress,
    )?;
    let snapshot_started = Instant::now();
    let html = html_with_base(&response.final_url, &response.body);
    let manifest = load_index_manifest_for_response(fetcher, &response);
    enforce_stage_budget(
        RuntimeStage::Snapshotting,
        snapshot_started.elapsed(),
        response.final_url.as_str(),
    )?;

    report_stage(
        &mut flow,
        RuntimeStage::Parsing,
        response.final_url.as_str(),
        report_progress,
    )?;
    let parse_started = Instant::now();
    enforce_stage_budget(
        RuntimeStage::Parsing,
        parse_started.elapsed(),
        response.final_url.as_str(),
    )?;

    report_stage(
        &mut flow,
        RuntimeStage::Transforming,
        response.final_url.as_str(),
        report_progress,
    )?;
    let transform_started = Instant::now();
    let transformed = transform_html_cached(cache, Some(response.final_url.as_str()), html);
    let mut document = hydrate_document_images(transformed);
    if let Some(manifest) = manifest.as_ref() {
        apply_index_manifest_hints(&mut document, manifest);
    }
    apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
    enforce_stage_budget(
        RuntimeStage::Transforming,
        transform_started.elapsed(),
        response.final_url.as_str(),
    )?;

    report_stage(
        &mut flow,
        RuntimeStage::Scoring,
        response.final_url.as_str(),
        report_progress,
    )?;
    let log = response_log_entry(
        sequence,
        "GET",
        &normalized,
        response.final_url.as_str(),
        response.mime_type.as_deref(),
        &response.body,
    );

    report_stage(
        &mut flow,
        RuntimeStage::Storing,
        response.final_url.as_str(),
        report_progress,
    )?;
    let store_started = Instant::now();
    if let Ok(artifact) = IndexArtifact::from_document(
        &document,
        &canonical,
        &response.final_url,
        ArtifactContext::LiveGet,
        unix_timestamp_secs(),
        ARTIFACT_MAX_AGE_SECS,
    ) {
        let _ = artifact_store.store(&artifact);
    }
    enforce_stage_budget(
        RuntimeStage::Storing,
        store_started.elapsed(),
        response.final_url.as_str(),
    )?;
    report_stage(
        &mut flow,
        RuntimeStage::Done,
        response.final_url.as_str(),
        report_progress,
    )?;

    Ok(TuiDocumentResult::new(document)
        .with_visited_url(response.final_url.as_str())
        .with_response_log(log))
}

#[cfg(test)]
fn fetch_document_cached_with_log_with_counter_with_progress<F: Fetcher>(
    fetcher: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &mut u64,
    target: &str,
    report_progress: &mut dyn FnMut(String),
) -> Result<TuiDocumentResult, String> {
    let normalized = normalize_url_input(target);
    report_progress(format!("fetching {normalized}"));
    let url = IndexUrl::parse(&normalized).map_err(|error| error.to_string())?;
    let response = fetcher
        .fetch(&Request { url })
        .map_err(|error| error.to_string())?;
    report_progress(format!("parsing {}", response.final_url.as_str()));
    let html = html_with_base(&response.final_url, &response.body);
    report_progress(format!("transforming {}", response.final_url.as_str()));
    let transformed = transform_html_cached(cache, Some(response.final_url.as_str()), html);
    report_progress(format!("rendering {}", response.final_url.as_str()));
    let mut document = hydrate_document_images(transformed);
    apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
    let log = response_log_entry(
        sequence,
        "GET",
        &normalized,
        response.final_url.as_str(),
        response.mime_type.as_deref(),
        &response.body,
    );
    Ok(TuiDocumentResult::new(document)
        .with_visited_url(response.final_url.as_str())
        .with_response_log(log))
}

#[cfg(test)]
fn submit_form_cached<F: FormSubmitter>(
    submitter: &F,
    cache: &mut TransformedDocumentCache,
    submission: &FormSubmission,
) -> Result<IndexDocument, String> {
    let response = submitter
        .submit_form(submission)
        .map_err(|error| error.to_string())?;
    let html = html_with_base(&response.final_url, &response.body);
    let mut document = transform_html_cached(cache, Some(response.final_url.as_str()), html);
    document = hydrate_document_images(document);
    apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
    Ok(document)
}

#[cfg(test)]
fn submit_form_cached_with_log<F: FormSubmitter>(
    submitter: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &RefCell<u64>,
    submission: &FormSubmission,
) -> Result<TuiDocumentResult, String> {
    let mut next_sequence = sequence.borrow_mut();
    submit_form_cached_with_log_with_counter(submitter, cache, &mut next_sequence, submission)
}

#[cfg(test)]
fn submit_form_cached_with_log_with_counter<F: FormSubmitter>(
    submitter: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &mut u64,
    submission: &FormSubmission,
) -> Result<TuiDocumentResult, String> {
    let mut ignore_progress = |_message: String| {};
    submit_form_cached_with_log_with_counter_with_progress(
        submitter,
        cache,
        sequence,
        submission,
        &mut ignore_progress,
    )
}

fn submit_form_with_artifact_runtime<F: FormSubmitter + Fetcher>(
    submitter: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &mut u64,
    artifact_store: &ArtifactStore,
    submission: &FormSubmission,
    report_progress: &mut dyn FnMut(String),
) -> Result<TuiDocumentResult, String> {
    let target = submission.action.as_str().to_owned();
    let mut flow = StageFlow::default();
    report_stage(&mut flow, RuntimeStage::Queued, &target, report_progress)?;
    report_stage(&mut flow, RuntimeStage::Fetching, &target, report_progress)?;
    let submit_started = Instant::now();
    let response = submitter.submit_form(submission).map_err(|error| {
        let _ = report_stage(&mut flow, RuntimeStage::Failed, &target, report_progress);
        error.to_string()
    })?;
    enforce_stage_budget(
        RuntimeStage::Fetching,
        submit_started.elapsed(),
        response.final_url.as_str(),
    )?;

    report_stage(
        &mut flow,
        RuntimeStage::Snapshotting,
        response.final_url.as_str(),
        report_progress,
    )?;
    let snapshot_started = Instant::now();
    let html = html_with_base(&response.final_url, &response.body);
    let manifest = load_index_manifest_for_response(submitter, &response);
    enforce_stage_budget(
        RuntimeStage::Snapshotting,
        snapshot_started.elapsed(),
        response.final_url.as_str(),
    )?;

    report_stage(
        &mut flow,
        RuntimeStage::Parsing,
        response.final_url.as_str(),
        report_progress,
    )?;
    let parse_started = Instant::now();
    enforce_stage_budget(
        RuntimeStage::Parsing,
        parse_started.elapsed(),
        response.final_url.as_str(),
    )?;

    report_stage(
        &mut flow,
        RuntimeStage::Transforming,
        response.final_url.as_str(),
        report_progress,
    )?;
    let transform_started = Instant::now();
    let transformed = transform_html_cached(cache, Some(response.final_url.as_str()), html);
    let mut document = hydrate_document_images(transformed);
    if let Some(manifest) = manifest.as_ref() {
        apply_index_manifest_hints(&mut document, manifest);
    }
    apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
    enforce_stage_budget(
        RuntimeStage::Transforming,
        transform_started.elapsed(),
        response.final_url.as_str(),
    )?;
    report_stage(
        &mut flow,
        RuntimeStage::Scoring,
        response.final_url.as_str(),
        report_progress,
    )?;

    let log = response_log_entry(
        sequence,
        submission.method.as_str(),
        submission.action.as_str(),
        response.final_url.as_str(),
        response.mime_type.as_deref(),
        &response.body,
    );

    report_stage(
        &mut flow,
        RuntimeStage::Storing,
        response.final_url.as_str(),
        report_progress,
    )?;
    let store_started = Instant::now();
    let canonical = submission.action.clone();
    if let Ok(artifact) = IndexArtifact::from_document(
        &document,
        &canonical,
        &response.final_url,
        ArtifactContext::LiveSubmit,
        unix_timestamp_secs(),
        ARTIFACT_MAX_AGE_SECS,
    ) {
        let _ = artifact_store.store(&artifact);
    }
    enforce_stage_budget(
        RuntimeStage::Storing,
        store_started.elapsed(),
        response.final_url.as_str(),
    )?;
    report_stage(
        &mut flow,
        RuntimeStage::Done,
        response.final_url.as_str(),
        report_progress,
    )?;

    Ok(TuiDocumentResult::new(document)
        .with_visited_url(response.final_url.as_str())
        .with_response_log(log))
}

#[cfg(test)]
fn submit_form_cached_with_log_with_counter_with_progress<F: FormSubmitter>(
    submitter: &F,
    cache: &mut TransformedDocumentCache,
    sequence: &mut u64,
    submission: &FormSubmission,
    report_progress: &mut dyn FnMut(String),
) -> Result<TuiDocumentResult, String> {
    report_progress(format!(
        "submitting {} {}",
        submission.method.as_str(),
        submission.action
    ));
    let response = submitter
        .submit_form(submission)
        .map_err(|error| error.to_string())?;
    report_progress(format!("parsing {}", response.final_url.as_str()));
    let html = html_with_base(&response.final_url, &response.body);
    report_progress(format!("transforming {}", response.final_url.as_str()));
    let transformed = transform_html_cached(cache, Some(response.final_url.as_str()), html);
    report_progress(format!("rendering {}", response.final_url.as_str()));
    let mut document = hydrate_document_images(transformed);
    apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
    let log = response_log_entry(
        sequence,
        submission.method.as_str(),
        submission.action.as_str(),
        response.final_url.as_str(),
        response.mime_type.as_deref(),
        &response.body,
    );
    Ok(TuiDocumentResult::new(document)
        .with_visited_url(response.final_url.as_str())
        .with_response_log(log))
}

fn document_from_index_artifact(
    cache: &mut TransformedDocumentCache,
    artifact: &IndexArtifact,
) -> Result<IndexDocument, String> {
    let final_url = IndexUrl::parse(artifact.final_url.replace("[REDACTED]", "redacted"))
        .map_err(|error| error.to_string())?;
    let transformed = transform_html_cached(
        cache,
        Some(final_url.as_str()),
        artifact.capture.redacted_html.clone(),
    );
    let mut document = hydrate_document_images(transformed);
    apply_runtime_pack_manifest_hints(&mut document, final_url.as_str());
    Ok(document)
}

fn document_from_index_artifact_uncached(
    artifact: &IndexArtifact,
) -> Result<IndexDocument, String> {
    let final_url = IndexUrl::parse(artifact.final_url.replace("[REDACTED]", "redacted"))
        .map_err(|error| error.to_string())?;
    let mut document = transform_html(html_with_base(&final_url, &artifact.capture.redacted_html));
    document = hydrate_document_images(document);
    apply_runtime_pack_manifest_hints(&mut document, final_url.as_str());
    Ok(document)
}

fn refresh_live_get_artifact<F: Fetcher>(
    fetcher: &F,
    artifact_store: &ArtifactStore,
    canonical: &IndexUrl,
) -> Result<(), String> {
    let response = fetcher
        .fetch(&Request {
            url: canonical.clone(),
        })
        .map_err(|error| error.to_string())?;
    let mut document = hydrate_document_images(transform_html(html_with_base(
        &response.final_url,
        &response.body,
    )));
    apply_runtime_pack_manifest_hints(&mut document, response.final_url.as_str());
    let artifact = IndexArtifact::from_document(
        &document,
        canonical,
        &response.final_url,
        ArtifactContext::LiveGet,
        unix_timestamp_secs(),
        ARTIFACT_MAX_AGE_SECS,
    )
    .map_err(|error| error.to_string())?;
    artifact_store
        .store(&artifact)
        .map_err(|error| error.to_string())?;
    Ok(())
}

fn response_log_entry(
    sequence: &mut u64,
    method: &str,
    requested_url: &str,
    final_url: &str,
    mime_type: Option<&str>,
    body: &str,
) -> ResponseLogEntry {
    *sequence = sequence.saturating_add(1);
    ResponseLogEntry::new(
        *sequence,
        method,
        requested_url,
        final_url,
        mime_type,
        body,
        512,
    )
}

fn transform_html(html: String) -> IndexDocument {
    Transformer::<Empty>::new()
        .fetched(html)
        .parse()
        .extract()
        .transform()
        .into_document()
}

fn hydrate_document_images(document: IndexDocument) -> IndexDocument {
    let agent: ureq::Agent = ureq::Agent::config_builder()
        .https_only(false)
        .timeout_global(Some(IMAGE_PREVIEW_TIMEOUT))
        .user_agent("Index/0.1.0")
        .build()
        .into();
    let mut loader = |src: &str| fetch_image_bytes(&agent, src);
    hydrate_document_images_with_loader(document, &mut loader)
}

fn hydrate_document_images_with_loader<L>(
    mut document: IndexDocument,
    loader: &mut L,
) -> IndexDocument
where
    L: FnMut(&str) -> Result<Vec<u8>, String>,
{
    let mut budget = IMAGE_PREVIEW_MAX_IMAGES_PER_DOCUMENT;
    document.nodes = hydrate_image_nodes(mem::take(&mut document.nodes), loader, &mut budget);
    document
}

fn hydrate_image_nodes<L>(
    nodes: Vec<IndexNode>,
    loader: &mut L,
    budget: &mut usize,
) -> Vec<IndexNode>
where
    L: FnMut(&str) -> Result<Vec<u8>, String>,
{
    let mut output = Vec::with_capacity(nodes.len());
    for node in nodes {
        match node {
            IndexNode::Section {
                role,
                title,
                collapsed,
                nodes,
            } => output.push(IndexNode::Section {
                role,
                title,
                collapsed,
                nodes: hydrate_image_nodes(nodes, loader, budget),
            }),
            IndexNode::Image { alt, src } => {
                if *budget > 0 {
                    if let Some(src_url) = src.as_deref() {
                        *budget = budget.saturating_sub(1);
                        if let Some(preview_nodes) =
                            hydrate_image_node_preview(&alt, src_url, loader)
                        {
                            output.extend(preview_nodes);
                            continue;
                        }
                    }
                }
                output.push(IndexNode::Image { alt, src });
            }
            other => output.push(other),
        }
    }
    output
}

fn hydrate_image_node_preview<L>(alt: &str, src: &str, loader: &mut L) -> Option<Vec<IndexNode>>
where
    L: FnMut(&str) -> Result<Vec<u8>, String>,
{
    let url = IndexUrl::parse(src).ok()?;
    let bytes = loader(url.as_str()).ok()?;
    let preview = dither_image_to_terminal_blocks(&bytes).ok()?;
    if preview.trim().is_empty() {
        return None;
    }

    let label = if alt.trim().is_empty() {
        "image preview".to_owned()
    } else {
        format!("image preview: {alt}")
    };

    Some(vec![
        IndexNode::Paragraph(format!("󰥶 {label}")),
        IndexNode::CodeBlock {
            language: Some("bw-dither".to_owned()),
            code: preview,
        },
        IndexNode::Link(Link::new("image source", url.as_str())),
    ])
}

fn fetch_image_bytes(agent: &ureq::Agent, src: &str) -> Result<Vec<u8>, String> {
    let mut response = agent.get(src).call().map_err(|error| error.to_string())?;
    let bytes = response
        .body_mut()
        .with_config()
        .limit(IMAGE_PREVIEW_MAX_FETCH_BYTES as u64)
        .read_to_vec()
        .map_err(|error| error.to_string())?;
    if bytes.is_empty() {
        return Err("empty image response".to_owned());
    }
    Ok(bytes)
}

fn dither_image_to_terminal_blocks(bytes: &[u8]) -> Result<String, String> {
    let image = image::load_from_memory(bytes).map_err(|error| error.to_string())?;
    let grayscale = image.to_luma8();
    let (width, height) = grayscale.dimensions();
    if width == 0 || height == 0 {
        return Err("image has zero dimensions".to_owned());
    }

    let (resized_width, resized_height) = image_preview_dimensions(width, height);
    let mut resized = resize(
        &grayscale,
        resized_width.max(1),
        resized_height.max(1),
        FilterType::Triangle,
    );
    dither(&mut resized, &BiLevel);

    Ok(half_block_render(&resized))
}

fn image_preview_dimensions(width: u32, height: u32) -> (u32, u32) {
    let width_scale = width as f32 / IMAGE_PREVIEW_MAX_WIDTH as f32;
    let height_scale = height as f32 / IMAGE_PREVIEW_MAX_HEIGHT as f32;
    let scale = width_scale.max(height_scale).max(1.0);
    let scaled_width = ((width as f32 / scale).round() as u32).clamp(1, IMAGE_PREVIEW_MAX_WIDTH);
    let scaled_height = ((height as f32 / scale).round() as u32).clamp(1, IMAGE_PREVIEW_MAX_HEIGHT);
    (scaled_width, scaled_height)
}

fn half_block_render(image: &image::GrayImage) -> String {
    let width = image.width() as usize;
    let height = image.height() as usize;
    let mut lines = Vec::new();
    let mut row = 0usize;
    while row < height {
        let mut line = String::with_capacity(width);
        for column in 0..width {
            let top = image.get_pixel(column as u32, row as u32)[0] == 0;
            let bottom = if row + 1 < height {
                image.get_pixel(column as u32, (row + 1) as u32)[0] == 0
            } else {
                false
            };
            let symbol = match (top, bottom) {
                (true, true) => '█',
                (true, false) => '▀',
                (false, true) => '▄',
                (false, false) => ' ',
            };
            line.push(symbol);
        }
        lines.push(line.trim_end_matches(' ').to_owned());
        row += 2;
    }
    while matches!(lines.last(), Some(last) if last.is_empty()) {
        let _ = lines.pop();
    }
    lines.join("\n")
}

fn run_save_command<F: Fetcher>(
    format: ExtractFormat,
    input: &str,
    output_path: &str,
    stdin: &mut dyn Read,
    fetcher: &F,
) -> Result<CliAction, String> {
    let document = load_document(input, stdin, fetcher)?;
    let output = try_extract_document(&document, format, ExtractionLimits::default())
        .map_err(|error| error.to_string())?;
    fs::write(output_path, output).map_err(|error| error.to_string())?;
    Ok(CliAction::Print(format!(
        "saved\t{}\t{}",
        format.as_str(),
        output_path
    )))
}

fn run_batch_extract_command(
    format: ExtractFormat,
    inputs: &[String],
) -> Result<CliAction, String> {
    let mut output = String::new();
    for input in inputs {
        let document = load_offline_document(input)?;
        output.push_str("==> ");
        output.push_str(input);
        output.push_str(" <==\n");
        output.push_str(
            &try_extract_document(&document, format, ExtractionLimits::default())
                .map_err(|error| error.to_string())?,
        );
        if !output.ends_with('\n') {
            output.push('\n');
        }
    }
    Ok(CliAction::Print(output))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CompatibilityCorpus {
    Top100,
    Forum,
}

impl CompatibilityCorpus {
    const fn as_str(self) -> &'static str {
        match self {
            Self::Top100 => "top100",
            Self::Forum => "forum",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CoverageExpectedPath {
    Generic,
    Adapter,
    Failure,
}

impl CoverageExpectedPath {
    fn parse(value: &str, context: &str) -> Result<Self, String> {
        match value {
            "generic" => Ok(Self::Generic),
            "adapter" => Ok(Self::Adapter),
            "failure" => Ok(Self::Failure),
            _ => Err(format!("invalid expected path for {context}: {value}")),
        }
    }

    const fn as_str(self) -> &'static str {
        match self {
            Self::Generic => "generic",
            Self::Adapter => "adapter",
            Self::Failure => "failure",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CoverageStatus {
    Covered,
    Partial,
    Blocked,
    Planned,
}

impl CoverageStatus {
    fn parse(value: &str, context: &str) -> Result<Self, String> {
        match value {
            "covered" => Ok(Self::Covered),
            "partial" => Ok(Self::Partial),
            "blocked" => Ok(Self::Blocked),
            "planned" => Ok(Self::Planned),
            _ => Err(format!("invalid coverage status for {context}: {value}")),
        }
    }

    const fn is_failure_guardrail_status(self) -> bool {
        matches!(self, Self::Blocked | Self::Planned)
    }

    const fn as_str(self) -> &'static str {
        match self {
            Self::Covered => "covered",
            Self::Partial => "partial",
            Self::Blocked => "blocked",
            Self::Planned => "planned",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CompatibilitySloRow {
    corpus: CompatibilityCorpus,
    domain: String,
    family: String,
    intent: Option<String>,
    min_tier: u8,
    current_tier: u8,
    expected_path: CoverageExpectedPath,
    status: CoverageStatus,
    known_limit: String,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct CompatibilitySloThresholds {
    readability_floor: f64,
    actionability_floor: f64,
    failure_quality_floor: f64,
}

impl Default for CompatibilitySloThresholds {
    fn default() -> Self {
        Self {
            readability_floor: COMPATIBILITY_SLO_READABILITY_FLOOR,
            actionability_floor: COMPATIBILITY_SLO_ACTIONABILITY_FLOOR,
            failure_quality_floor: COMPATIBILITY_SLO_FAILURE_QUALITY_FLOOR,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CorpusCompatibilityScore {
    corpus: CompatibilityCorpus,
    total_rows: usize,
    eligible_rows: usize,
    readable_rows: usize,
    actionable_rows: usize,
    failure_rows: usize,
    guarded_failure_rows: usize,
    guardrail_failures: Vec<String>,
}

#[derive(Debug, Clone, PartialEq)]
struct CompatibilitySloReport {
    top100_path: String,
    forum_path: String,
    thresholds: CompatibilitySloThresholds,
    top100: CorpusCompatibilityScore,
    forum: CorpusCompatibilityScore,
    readability_score: f64,
    actionability_score: f64,
    failure_quality_score: f64,
    violations: Vec<String>,
}

impl CompatibilitySloReport {
    fn passed(&self) -> bool {
        self.violations.is_empty()
    }

    fn to_text(&self) -> String {
        let status = if self.passed() { "pass" } else { "fail" };
        let mut output = vec![
            "index-compatibility-slo-v1".to_owned(),
            format!("top100_matrix: {}", self.top100_path),
            format!("forum_matrix: {}", self.forum_path),
            format!(
                "thresholds: readability>={:.4} actionability>={:.4} failure_quality>={:.4}",
                self.thresholds.readability_floor,
                self.thresholds.actionability_floor,
                self.thresholds.failure_quality_floor
            ),
            format!(
                "scores: readability={:.4} ({}/{}) actionability={:.4} ({}/{}) failure_quality={:.4} ({}/{})",
                self.readability_score,
                self.top100.readable_rows + self.forum.readable_rows,
                self.top100.eligible_rows + self.forum.eligible_rows,
                self.actionability_score,
                self.top100.actionable_rows + self.forum.actionable_rows,
                self.top100.eligible_rows + self.forum.eligible_rows,
                self.failure_quality_score,
                self.top100.guarded_failure_rows + self.forum.guarded_failure_rows,
                self.top100.failure_rows + self.forum.failure_rows,
            ),
            format!(
                "corpus.top100: rows={} eligible={} readable={} actionable={} failures={} guarded_failures={}",
                self.top100.total_rows,
                self.top100.eligible_rows,
                self.top100.readable_rows,
                self.top100.actionable_rows,
                self.top100.failure_rows,
                self.top100.guarded_failure_rows,
            ),
            format!(
                "corpus.forum: rows={} eligible={} readable={} actionable={} failures={} guarded_failures={}",
                self.forum.total_rows,
                self.forum.eligible_rows,
                self.forum.readable_rows,
                self.forum.actionable_rows,
                self.forum.failure_rows,
                self.forum.guarded_failure_rows,
            ),
        ];
        if self.violations.is_empty() {
            output.push("violations: none".to_owned());
        } else {
            output.push("violations:".to_owned());
            for violation in &self.violations {
                output.push(format!("- {violation}"));
            }
        }
        output.push(format!("result: {status}"));
        output.join("\n")
    }
}

#[derive(Debug, Clone, PartialEq)]
struct FamilyCompatibilityScore {
    family: String,
    rows_total: usize,
    eligible_rows: usize,
    readable_rows: usize,
    actionable_rows: usize,
    failure_rows: usize,
    guarded_failure_rows: usize,
    readability_score: f64,
    actionability_score: f64,
    failure_quality_score: f64,
    thresholds: CompatibilitySloThresholds,
    violations: Vec<String>,
    requires_non_failure_evidence: bool,
}

#[derive(Debug, Clone, PartialEq)]
struct CompatibilitySloFamilyDelta {
    family: String,
    readability_delta: f64,
    actionability_delta: f64,
    failure_quality_delta: f64,
}

#[derive(Debug, Clone, PartialEq)]
struct CompatibilitySloDeltaReport {
    readability_delta: f64,
    actionability_delta: f64,
    failure_quality_delta: f64,
    families: Vec<CompatibilitySloFamilyDelta>,
}

#[derive(Debug, Clone, PartialEq)]
struct CompatibilitySloV2Report {
    top100_path: String,
    forum_path: String,
    baseline_top100_path: Option<String>,
    baseline_forum_path: Option<String>,
    global: CompatibilitySloReport,
    families: Vec<FamilyCompatibilityScore>,
    delta: Option<CompatibilitySloDeltaReport>,
    violations: Vec<String>,
    evidence_warnings: Vec<String>,
    planning_inputs: Vec<String>,
}

impl CompatibilitySloV2Report {
    fn passed(&self) -> bool {
        self.violations.is_empty()
    }

    fn to_text(&self) -> String {
        let status = if self.passed() { "pass" } else { "fail" };
        let mut output = vec![
            "index-compatibility-slo-v2".to_owned(),
            format!("top100_matrix: {}", self.top100_path),
            format!("forum_matrix: {}", self.forum_path),
        ];
        if let (Some(top100), Some(forum)) = (
            self.baseline_top100_path.as_deref(),
            self.baseline_forum_path.as_deref(),
        ) {
            output.push(format!("baseline_top100_matrix: {top100}"));
            output.push(format!("baseline_forum_matrix: {forum}"));
        }
        output.push(format!(
            "global_scores: readability={:.4} actionability={:.4} failure_quality={:.4}",
            self.global.readability_score,
            self.global.actionability_score,
            self.global.failure_quality_score
        ));
        output.push(format!("family_rows: {}", self.families.len()));
        output.push("family_columns: family\treadability\treadability_floor\tactionability\tactionability_floor\tfailure_quality\tfailure_quality_floor\teligible_rows\tfailure_rows\tresult".to_owned());
        for family in &self.families {
            output.push(format!(
                "{}\t{:.4}\t{:.4}\t{:.4}\t{:.4}\t{:.4}\t{:.4}\t{}\t{}\t{}",
                family.family,
                family.readability_score,
                family.thresholds.readability_floor,
                family.actionability_score,
                family.thresholds.actionability_floor,
                family.failure_quality_score,
                family.thresholds.failure_quality_floor,
                family.eligible_rows,
                family.failure_rows,
                if family.violations.is_empty() {
                    "pass"
                } else {
                    "fail"
                },
            ));
        }
        if let Some(delta) = &self.delta {
            output.push(format!(
                "delta_global: readability={:+.4} actionability={:+.4} failure_quality={:+.4}",
                delta.readability_delta, delta.actionability_delta, delta.failure_quality_delta
            ));
            output.push("delta_family_columns: family\treadability_delta\tactionability_delta\tfailure_quality_delta".to_owned());
            for family in &delta.families {
                output.push(format!(
                    "{}\t{:+.4}\t{:+.4}\t{:+.4}",
                    family.family,
                    family.readability_delta,
                    family.actionability_delta,
                    family.failure_quality_delta
                ));
            }
        }
        if self.violations.is_empty() {
            output.push("violations: none".to_owned());
        } else {
            output.push("violations:".to_owned());
            for violation in &self.violations {
                output.push(format!("- {violation}"));
            }
        }
        if self.evidence_warnings.is_empty() {
            output.push("evidence_warnings: none".to_owned());
        } else {
            output.push("evidence_warnings:".to_owned());
            for warning in &self.evidence_warnings {
                output.push(format!("- {warning}"));
            }
        }
        if self.planning_inputs.is_empty() {
            output.push("planning_inputs: none".to_owned());
        } else {
            output.push("planning_inputs:".to_owned());
            for input in &self.planning_inputs {
                output.push(format!("- {input}"));
            }
        }
        output.push(format!("result: {status}"));
        output.join("\n")
    }
}

fn run_compatibility_slo_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let (mut top100_path, mut forum_path) = default_compatibility_slo_paths();
    let mut thresholds = CompatibilitySloThresholds::default();

    while let Some(argument) = args.next() {
        match argument.as_ref() {
            "--top100" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                top100_path = path.as_ref().to_owned();
            }
            "--forum" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                forum_path = path.as_ref().to_owned();
            }
            "--min-readability" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.readability_floor = parse_slo_floor("readability", value.as_ref())?;
            }
            "--min-actionability" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.actionability_floor = parse_slo_floor("actionability", value.as_ref())?;
            }
            "--min-failure-quality" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.failure_quality_floor =
                    parse_slo_floor("failure-quality", value.as_ref())?;
            }
            other => return Err(format!("unsupported compatibility-slo option: {other}")),
        }
    }

    let report = compatibility_slo_report_from_paths(&top100_path, &forum_path, thresholds)?;
    if report.passed() {
        Ok(CliAction::Print(report.to_text()))
    } else {
        Err(report.to_text())
    }
}

fn run_compatibility_slo_v2_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let (mut top100_path, mut forum_path) = default_compatibility_slo_paths();
    let mut thresholds = CompatibilitySloThresholds::default();
    let mut baseline_top100_path: Option<String> = None;
    let mut baseline_forum_path: Option<String> = None;

    while let Some(argument) = args.next() {
        match argument.as_ref() {
            "--top100" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                top100_path = path.as_ref().to_owned();
            }
            "--forum" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                forum_path = path.as_ref().to_owned();
            }
            "--baseline-top100" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                baseline_top100_path = Some(path.as_ref().to_owned());
            }
            "--baseline-forum" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                baseline_forum_path = Some(path.as_ref().to_owned());
            }
            "--min-readability" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.readability_floor = parse_slo_floor("readability", value.as_ref())?;
            }
            "--min-actionability" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.actionability_floor = parse_slo_floor("actionability", value.as_ref())?;
            }
            "--min-failure-quality" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.failure_quality_floor =
                    parse_slo_floor("failure-quality", value.as_ref())?;
            }
            other => return Err(format!("unsupported compatibility-slo-v2 option: {other}")),
        }
    }

    if baseline_top100_path.is_some() != baseline_forum_path.is_some() {
        return Err(
            "compatibility-slo-v2 requires both --baseline-top100 and --baseline-forum".to_owned(),
        );
    }

    let report = compatibility_slo_v2_report_from_paths(
        &top100_path,
        &forum_path,
        thresholds,
        baseline_top100_path.as_deref(),
        baseline_forum_path.as_deref(),
    )?;

    if report.passed() {
        Ok(CliAction::Print(report.to_text()))
    } else {
        Err(report.to_text())
    }
}

fn default_compatibility_slo_paths() -> (String, String) {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
    let top100 = root.join(COMPATIBILITY_SLO_TOP100_PATH);
    let forum = root.join(COMPATIBILITY_SLO_FORUM_PATH);
    (top100.display().to_string(), forum.display().to_string())
}

fn parse_slo_floor(name: &str, value: &str) -> Result<f64, String> {
    let parsed = value
        .parse::<f64>()
        .map_err(|error| format!("invalid {name} floor value {value}: {error}"))?;
    if !(0.0..=1.0).contains(&parsed) {
        return Err(format!(
            "invalid {name} floor value {value}: must be between 0.0 and 1.0"
        ));
    }
    Ok(parsed)
}

fn compatibility_slo_report_from_paths(
    top100_path: &str,
    forum_path: &str,
    thresholds: CompatibilitySloThresholds,
) -> Result<CompatibilitySloReport, String> {
    let top100_text = fs::read_to_string(top100_path)
        .map_err(|error| format!("failed to read {top100_path}: {error}"))?;
    let forum_text = fs::read_to_string(forum_path)
        .map_err(|error| format!("failed to read {forum_path}: {error}"))?;
    let top100_rows = parse_top100_slo_rows(&top100_text)?;
    let forum_rows = parse_forum_slo_rows(&forum_text)?;
    Ok(evaluate_compatibility_slo(
        top100_path,
        forum_path,
        &top100_rows,
        &forum_rows,
        thresholds,
    ))
}

fn compatibility_slo_v2_report_from_paths(
    top100_path: &str,
    forum_path: &str,
    thresholds: CompatibilitySloThresholds,
    baseline_top100_path: Option<&str>,
    baseline_forum_path: Option<&str>,
) -> Result<CompatibilitySloV2Report, String> {
    let top100_text = fs::read_to_string(top100_path)
        .map_err(|error| format!("failed to read {top100_path}: {error}"))?;
    let forum_text = fs::read_to_string(forum_path)
        .map_err(|error| format!("failed to read {forum_path}: {error}"))?;
    let top100_rows = parse_top100_slo_rows(&top100_text)?;
    let forum_rows = parse_forum_slo_rows(&forum_text)?;

    let baseline_rows = if let (Some(baseline_top100_path), Some(baseline_forum_path)) =
        (baseline_top100_path, baseline_forum_path)
    {
        let top100_baseline_text = fs::read_to_string(baseline_top100_path)
            .map_err(|error| format!("failed to read {baseline_top100_path}: {error}"))?;
        let forum_baseline_text = fs::read_to_string(baseline_forum_path)
            .map_err(|error| format!("failed to read {baseline_forum_path}: {error}"))?;
        Some((
            parse_top100_slo_rows(&top100_baseline_text)?,
            parse_forum_slo_rows(&forum_baseline_text)?,
        ))
    } else {
        None
    };

    let baseline = match (
        baseline_top100_path,
        baseline_forum_path,
        baseline_rows.as_ref(),
    ) {
        (Some(top100_path), Some(forum_path), Some((top100_rows, forum_rows))) => {
            Some(CompatibilitySloV2Baseline {
                top100_path,
                forum_path,
                top100_rows,
                forum_rows,
            })
        }
        _ => None,
    };

    Ok(evaluate_compatibility_slo_v2(
        top100_path,
        forum_path,
        &top100_rows,
        &forum_rows,
        thresholds,
        baseline,
    ))
}

#[derive(Debug, Clone, Copy)]
struct CompatibilitySloV2Baseline<'a> {
    top100_path: &'a str,
    forum_path: &'a str,
    top100_rows: &'a [CompatibilitySloRow],
    forum_rows: &'a [CompatibilitySloRow],
}

fn evaluate_compatibility_slo_v2(
    top100_path: &str,
    forum_path: &str,
    top100_rows: &[CompatibilitySloRow],
    forum_rows: &[CompatibilitySloRow],
    thresholds: CompatibilitySloThresholds,
    baseline: Option<CompatibilitySloV2Baseline<'_>>,
) -> CompatibilitySloV2Report {
    let global =
        evaluate_compatibility_slo(top100_path, forum_path, top100_rows, forum_rows, thresholds);

    let families = score_compatibility_families(top100_rows, forum_rows);
    let mut violations = global.violations.clone();
    for family in &families {
        for violation in &family.violations {
            violations.push(format!("family {}: {violation}", family.family));
        }
    }

    let delta = baseline.map(|baseline| {
        compatibility_slo_delta(
            top100_path,
            forum_path,
            top100_rows,
            forum_rows,
            baseline.top100_rows,
            baseline.forum_rows,
            thresholds,
        )
    });
    if let Some(delta) = &delta {
        if delta.readability_delta < 0.0 {
            violations.push(format!(
                "global readability regressed by {:.4}",
                delta.readability_delta
            ));
        }
        if delta.actionability_delta < 0.0 {
            violations.push(format!(
                "global actionability regressed by {:.4}",
                delta.actionability_delta
            ));
        }
        if delta.failure_quality_delta < 0.0 {
            violations.push(format!(
                "global failure-quality regressed by {:.4}",
                delta.failure_quality_delta
            ));
        }
    }

    let evidence_warnings = compatibility_slo_evidence_warnings(&families);
    let planning_inputs =
        compatibility_slo_planning_inputs(&families, delta.as_ref(), &evidence_warnings);

    CompatibilitySloV2Report {
        top100_path: top100_path.to_owned(),
        forum_path: forum_path.to_owned(),
        baseline_top100_path: baseline.map(|value| value.top100_path.to_owned()),
        baseline_forum_path: baseline.map(|value| value.forum_path.to_owned()),
        global,
        families,
        delta,
        violations,
        evidence_warnings,
        planning_inputs,
    }
}

fn compatibility_slo_evidence_warnings(families: &[FamilyCompatibilityScore]) -> Vec<String> {
    families
        .iter()
        .filter(|family| {
            family.rows_total > 0
                && family.eligible_rows == 0
                && family.requires_non_failure_evidence
        })
        .map(|family| {
            format!(
                "family {} has no known_limit=none rows (rows_total={}, failure_rows={}); add at least one non-limited fixture row",
                family.family, family.rows_total, family.failure_rows
            )
        })
        .collect()
}

fn canonical_family_name(family: &str) -> String {
    match family.trim().to_ascii_lowercase().as_str() {
        "reddit" | "generic-forum" => "social-community".to_owned(),
        _ => family.trim().to_owned(),
    }
}

fn score_compatibility_families(
    top100_rows: &[CompatibilitySloRow],
    forum_rows: &[CompatibilitySloRow],
) -> Vec<FamilyCompatibilityScore> {
    use std::collections::BTreeSet;

    let mut family_names = BTreeSet::new();
    for row in top100_rows {
        family_names.insert(canonical_family_name(&row.family));
    }
    for row in forum_rows {
        family_names.insert(canonical_family_name(&row.family));
    }

    family_names
        .into_iter()
        .map(|family| {
            let rows = top100_rows
                .iter()
                .chain(forum_rows.iter())
                .filter(|row| canonical_family_name(&row.family) == family)
                .collect::<Vec<_>>();
            score_compatibility_family(&family, &rows)
        })
        .collect()
}

fn score_compatibility_family(
    family: &str,
    rows: &[&CompatibilitySloRow],
) -> FamilyCompatibilityScore {
    let mut eligible_rows = 0usize;
    let mut readable_rows = 0usize;
    let mut actionable_rows = 0usize;
    let mut failure_rows = 0usize;
    let mut guarded_failure_rows = 0usize;
    let mut guardrail_failures = Vec::new();
    let mut requires_non_failure_evidence = false;
    for row in rows {
        if row.min_tier > 0 {
            requires_non_failure_evidence = true;
        }
        if row.known_limit == "none" {
            eligible_rows += 1;
            if row.current_tier >= 1 {
                readable_rows += 1;
            }
            if row.current_tier >= 2 {
                actionable_rows += 1;
            }
        }
        if row.expected_path == CoverageExpectedPath::Failure {
            failure_rows += 1;
            let guarded = row.known_limit != "none"
                && row.status.is_failure_guardrail_status()
                && row.current_tier <= 1
                && row.min_tier <= 1;
            if guarded {
                guarded_failure_rows += 1;
            } else {
                guardrail_failures.push(format!("{}:{}", row.corpus.as_str(), row.domain));
            }
        }
    }

    let thresholds = family_thresholds(family);
    let readability_score = ratio(readable_rows, eligible_rows);
    let actionability_score = ratio(actionable_rows, eligible_rows);
    let failure_quality_score = ratio(guarded_failure_rows, failure_rows);
    let mut violations = Vec::new();
    if eligible_rows > 0 {
        if readability_score < thresholds.readability_floor {
            violations.push(format!(
                "readability {:.4} below floor {:.4}",
                readability_score, thresholds.readability_floor
            ));
        }
        if actionability_score < thresholds.actionability_floor {
            violations.push(format!(
                "actionability {:.4} below floor {:.4}",
                actionability_score, thresholds.actionability_floor
            ));
        }
    }
    if failure_rows > 0 && failure_quality_score < thresholds.failure_quality_floor {
        violations.push(format!(
            "failure-quality {:.4} below floor {:.4}",
            failure_quality_score, thresholds.failure_quality_floor
        ));
    }
    if !guardrail_failures.is_empty() {
        violations.push(format!(
            "failure guardrail violations: {}",
            guardrail_failures.join(", ")
        ));
    }

    FamilyCompatibilityScore {
        family: family.to_owned(),
        rows_total: rows.len(),
        eligible_rows,
        readable_rows,
        actionable_rows,
        failure_rows,
        guarded_failure_rows,
        readability_score,
        actionability_score,
        failure_quality_score,
        thresholds,
        violations,
        requires_non_failure_evidence,
    }
}

fn family_thresholds(family: &str) -> CompatibilitySloThresholds {
    let normalized = family.trim().to_ascii_lowercase();
    if normalized.contains("search")
        || normalized.contains("stackexchange")
        || normalized.contains("hacker-news")
        || normalized.contains("slashdot")
    {
        CompatibilitySloThresholds {
            readability_floor: 0.35,
            actionability_floor: 0.20,
            failure_quality_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_FAILURE_FLOOR,
        }
    } else if normalized.contains("commerce") || normalized.contains("marketplace") {
        CompatibilitySloThresholds {
            readability_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_READABILITY_FLOOR,
            actionability_floor: 0.05,
            failure_quality_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_FAILURE_FLOOR,
        }
    } else if normalized.contains("media") || normalized.contains("streaming") {
        CompatibilitySloThresholds {
            readability_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_READABILITY_FLOOR,
            actionability_floor: 0.14,
            failure_quality_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_FAILURE_FLOOR,
        }
    } else if normalized.contains("services") || normalized.contains("utility") {
        CompatibilitySloThresholds {
            readability_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_READABILITY_FLOOR,
            actionability_floor: 0.0,
            failure_quality_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_FAILURE_FLOOR,
        }
    } else if normalized.contains("knowledge") || normalized.contains("docs") {
        CompatibilitySloThresholds {
            readability_floor: 0.30,
            actionability_floor: 0.14,
            failure_quality_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_FAILURE_FLOOR,
        }
    } else {
        CompatibilitySloThresholds {
            readability_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_READABILITY_FLOOR,
            actionability_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_ACTIONABILITY_FLOOR,
            failure_quality_floor: COMPATIBILITY_SLO_V2_DEFAULT_FAMILY_FAILURE_FLOOR,
        }
    }
}

fn compatibility_slo_delta(
    top100_path: &str,
    forum_path: &str,
    top100_rows: &[CompatibilitySloRow],
    forum_rows: &[CompatibilitySloRow],
    baseline_top100_rows: &[CompatibilitySloRow],
    baseline_forum_rows: &[CompatibilitySloRow],
    thresholds: CompatibilitySloThresholds,
) -> CompatibilitySloDeltaReport {
    use std::collections::BTreeSet;

    let current =
        evaluate_compatibility_slo(top100_path, forum_path, top100_rows, forum_rows, thresholds);
    let baseline = evaluate_compatibility_slo(
        top100_path,
        forum_path,
        baseline_top100_rows,
        baseline_forum_rows,
        thresholds,
    );

    let current_family_scores = score_compatibility_families(top100_rows, forum_rows);
    let baseline_family_scores =
        score_compatibility_families(baseline_top100_rows, baseline_forum_rows);

    let mut families = BTreeSet::new();
    for score in &current_family_scores {
        families.insert(score.family.clone());
    }
    for score in &baseline_family_scores {
        families.insert(score.family.clone());
    }

    let family_deltas = families
        .into_iter()
        .map(|family| {
            let current_score = current_family_scores
                .iter()
                .find(|score| score.family == family);
            let baseline_score = baseline_family_scores
                .iter()
                .find(|score| score.family == family);
            CompatibilitySloFamilyDelta {
                family,
                readability_delta: current_score.map_or(0.0, |score| score.readability_score)
                    - baseline_score.map_or(0.0, |score| score.readability_score),
                actionability_delta: current_score.map_or(0.0, |score| score.actionability_score)
                    - baseline_score.map_or(0.0, |score| score.actionability_score),
                failure_quality_delta: current_score
                    .map_or(0.0, |score| score.failure_quality_score)
                    - baseline_score.map_or(0.0, |score| score.failure_quality_score),
            }
        })
        .collect::<Vec<_>>();

    CompatibilitySloDeltaReport {
        readability_delta: current.readability_score - baseline.readability_score,
        actionability_delta: current.actionability_score - baseline.actionability_score,
        failure_quality_delta: current.failure_quality_score - baseline.failure_quality_score,
        families: family_deltas,
    }
}

fn compatibility_slo_planning_inputs(
    families: &[FamilyCompatibilityScore],
    delta: Option<&CompatibilitySloDeltaReport>,
    evidence_warnings: &[String],
) -> Vec<String> {
    let mut inputs = Vec::new();

    for family in families {
        if family
            .violations
            .iter()
            .any(|violation| violation.starts_with("readability "))
        {
            inputs.push(format!("M74: readability gap for family {}", family.family));
        }
        if family
            .violations
            .iter()
            .any(|violation| violation.starts_with("actionability "))
        {
            if family.family.contains("app-shell")
                || family.family.contains("commerce")
                || family.family.contains("media")
            {
                inputs.push(format!(
                    "M78: family-pack actionability gap for family {}",
                    family.family
                ));
            } else {
                inputs.push(format!(
                    "M75: actionability gap for family {}",
                    family.family
                ));
            }
        }
        if family
            .violations
            .iter()
            .any(|violation| violation.starts_with("failure-quality "))
            || family
                .violations
                .iter()
                .any(|violation| violation.starts_with("failure guardrail violations"))
        {
            inputs.push(format!(
                "M76: failure-quality gap for family {}",
                family.family
            ));
        }
    }

    if let Some(delta) = delta {
        for family in &delta.families {
            if family.readability_delta < 0.0 {
                inputs.push(format!(
                    "M74: readability regression {:.4} for family {}",
                    family.readability_delta, family.family
                ));
            }
            if family.actionability_delta < 0.0 {
                inputs.push(format!(
                    "M75: actionability regression {:.4} for family {}",
                    family.actionability_delta, family.family
                ));
            }
            if family.failure_quality_delta < 0.0 {
                inputs.push(format!(
                    "M76: failure-quality regression {:.4} for family {}",
                    family.failure_quality_delta, family.family
                ));
            }
        }
    }
    for warning in evidence_warnings {
        if let Some(family) = warning
            .strip_prefix("family ")
            .and_then(|rest| rest.split_once(' '))
            .map(|(name, _)| name)
        {
            inputs.push(format!("M73: evidence-gap backlog for family {family}"));
        } else {
            inputs.push("M73: evidence-gap backlog".to_owned());
        }
    }

    inputs.sort();
    inputs.dedup();
    inputs
}

fn evaluate_compatibility_slo(
    top100_path: &str,
    forum_path: &str,
    top100_rows: &[CompatibilitySloRow],
    forum_rows: &[CompatibilitySloRow],
    thresholds: CompatibilitySloThresholds,
) -> CompatibilitySloReport {
    let top100 = score_compatibility_corpus(CompatibilityCorpus::Top100, top100_rows);
    let forum = score_compatibility_corpus(CompatibilityCorpus::Forum, forum_rows);
    let readable_rows = top100.readable_rows + forum.readable_rows;
    let actionable_rows = top100.actionable_rows + forum.actionable_rows;
    let eligible_rows = top100.eligible_rows + forum.eligible_rows;
    let guarded_failure_rows = top100.guarded_failure_rows + forum.guarded_failure_rows;
    let failure_rows = top100.failure_rows + forum.failure_rows;
    let readability_score = ratio(readable_rows, eligible_rows);
    let actionability_score = ratio(actionable_rows, eligible_rows);
    let failure_quality_score = ratio(guarded_failure_rows, failure_rows);

    let mut violations = Vec::new();
    if eligible_rows == 0 {
        violations.push(
            "eligible corpus rows are zero; readability/actionability are undefined".to_owned(),
        );
    }
    if failure_rows == 0 {
        violations.push(
            "failure corpus rows are zero; failure-quality guardrail is undefined".to_owned(),
        );
    }
    if readability_score < thresholds.readability_floor {
        violations.push(format!(
            "readability score {:.4} below floor {:.4}",
            readability_score, thresholds.readability_floor
        ));
    }
    if actionability_score < thresholds.actionability_floor {
        violations.push(format!(
            "actionability score {:.4} below floor {:.4}",
            actionability_score, thresholds.actionability_floor
        ));
    }
    if failure_quality_score < thresholds.failure_quality_floor {
        violations.push(format!(
            "failure-quality score {:.4} below floor {:.4}",
            failure_quality_score, thresholds.failure_quality_floor
        ));
    }
    let mut guardrail_failures = top100.guardrail_failures.clone();
    guardrail_failures.extend(forum.guardrail_failures.clone());
    guardrail_failures.sort();
    if !guardrail_failures.is_empty() {
        violations.push(format!(
            "failure guardrail violations in {} rows: {}",
            guardrail_failures.len(),
            guardrail_failures.join(", ")
        ));
    }

    CompatibilitySloReport {
        top100_path: top100_path.to_owned(),
        forum_path: forum_path.to_owned(),
        thresholds,
        top100,
        forum,
        readability_score,
        actionability_score,
        failure_quality_score,
        violations,
    }
}

fn score_compatibility_corpus(
    corpus: CompatibilityCorpus,
    rows: &[CompatibilitySloRow],
) -> CorpusCompatibilityScore {
    let mut score = CorpusCompatibilityScore {
        corpus,
        total_rows: rows.len(),
        eligible_rows: 0,
        readable_rows: 0,
        actionable_rows: 0,
        failure_rows: 0,
        guarded_failure_rows: 0,
        guardrail_failures: Vec::new(),
    };
    for row in rows {
        if row.known_limit == "none" {
            score.eligible_rows += 1;
            if row.current_tier >= 1 {
                score.readable_rows += 1;
            }
            if row.current_tier >= 2 {
                score.actionable_rows += 1;
            }
        }
        if row.expected_path == CoverageExpectedPath::Failure {
            score.failure_rows += 1;
            let guarded = row.known_limit != "none"
                && row.status.is_failure_guardrail_status()
                && row.current_tier <= 1
                && row.min_tier <= 1;
            if guarded {
                score.guarded_failure_rows += 1;
            } else {
                score
                    .guardrail_failures
                    .push(format!("{}:{}", row.corpus.as_str(), row.domain));
            }
        }
    }
    score
}

fn ratio(numerator: usize, denominator: usize) -> f64 {
    if denominator == 0 {
        return 0.0;
    }
    numerator as f64 / denominator as f64
}

fn parse_top100_slo_rows(text: &str) -> Result<Vec<CompatibilitySloRow>, String> {
    let mut rows = Vec::new();
    for (line_number, line) in text.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let fields = trimmed.split('\t').collect::<Vec<_>>();
        if fields.len() < 9 {
            return Err(format!(
                "invalid top100 matrix row at line {}: expected 9 fields, got {}",
                line_number + 1,
                fields.len()
            ));
        }
        let domain = fields[0].to_owned();
        let family = fields[1].trim().to_owned();
        let intent = fields[2].trim().to_owned();
        let context = format!("top100 row {} ({domain})", line_number + 1);
        let min_tier = parse_slo_tier(fields[3], &context)?;
        let current_tier = parse_slo_tier(fields[4], &context)?;
        let expected_path = CoverageExpectedPath::parse(fields[6], &context)?;
        let status = CoverageStatus::parse(fields[7], &context)?;
        let known_limit = fields[8].trim().to_owned();
        if family.is_empty() || intent.is_empty() || known_limit.is_empty() {
            return Err(format!("missing required top100 fields for {context}"));
        }
        rows.push(CompatibilitySloRow {
            corpus: CompatibilityCorpus::Top100,
            domain,
            family,
            intent: Some(intent),
            min_tier,
            current_tier,
            expected_path,
            status,
            known_limit,
        });
    }
    Ok(rows)
}

fn parse_forum_slo_rows(text: &str) -> Result<Vec<CompatibilitySloRow>, String> {
    let mut rows = Vec::new();
    for (line_number, line) in text.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let fields = trimmed.split('\t').collect::<Vec<_>>();
        if fields.len() < 8 {
            return Err(format!(
                "invalid forum matrix row at line {}: expected 8 fields, got {}",
                line_number + 1,
                fields.len()
            ));
        }
        let domain = fields[0].to_owned();
        let family = fields[1].trim().to_owned();
        let context = format!("forum row {} ({domain})", line_number + 1);
        let min_tier = parse_slo_tier(fields[2], &context)?;
        let current_tier = parse_slo_tier(fields[3], &context)?;
        let expected_path = CoverageExpectedPath::parse(fields[5], &context)?;
        let status = CoverageStatus::parse(fields[6], &context)?;
        let known_limit = fields[7].trim().to_owned();
        if family.is_empty() || known_limit.is_empty() {
            return Err(format!("missing required forum fields for {context}"));
        }
        rows.push(CompatibilitySloRow {
            corpus: CompatibilityCorpus::Forum,
            domain,
            family,
            intent: None,
            min_tier,
            current_tier,
            expected_path,
            status,
            known_limit,
        });
    }
    Ok(rows)
}

fn parse_slo_tier(value: &str, context: &str) -> Result<u8, String> {
    let tier = value
        .parse::<u8>()
        .map_err(|error| format!("invalid tier for {context}: {value} ({error})"))?;
    if tier > 5 {
        return Err(format!("invalid tier for {context}: {value}"));
    }
    Ok(tier)
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CompatibilityBacklogRow {
    corpus: CompatibilityCorpus,
    domain: String,
    family: String,
    intent: Option<String>,
    min_tier: u8,
    current_tier: u8,
    expected_path: CoverageExpectedPath,
    status: CoverageStatus,
    known_limit: String,
}

impl CompatibilityBacklogRow {
    fn priority_band(&self) -> u8 {
        if self.known_limit == "none" && self.current_tier < 2 {
            return 0;
        }
        if self.known_limit == "none" {
            return 1;
        }
        if self.expected_path == CoverageExpectedPath::Failure {
            return 2;
        }
        3
    }

    fn priority_key(&self) -> (u8, u8, u8, &str, &str, &str) {
        let intent = self.intent.as_deref().unwrap_or("");
        (
            self.priority_band(),
            self.current_tier,
            self.min_tier,
            self.family.as_str(),
            intent,
            self.domain.as_str(),
        )
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CompatibilityMilestone {
    M74,
    M75,
    M76,
    M78,
}

impl CompatibilityMilestone {
    const fn as_str(self) -> &'static str {
        match self {
            Self::M74 => "M74",
            Self::M75 => "M75",
            Self::M76 => "M76",
            Self::M78 => "M78",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CompatibilityBacklogEntry {
    row: CompatibilityBacklogRow,
    milestone: CompatibilityMilestone,
    rationale: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CompatibilityBacklogReport {
    top100_path: String,
    forum_path: String,
    top_n: usize,
    rows_total: usize,
    ranked: Vec<CompatibilityBacklogEntry>,
}

impl CompatibilityBacklogReport {
    fn to_text(&self) -> String {
        let mut output = vec![
            "index-compatibility-backlog-v1".to_owned(),
            format!("top100_matrix: {}", self.top100_path),
            format!("forum_matrix: {}", self.forum_path),
            format!("rows_total: {}", self.rows_total),
            format!("top_n: {}", self.top_n),
            "policy: known_limit=none first; lower current_tier first; deterministic tie-breakers".to_owned(),
            "columns: rank\tcorpus\tdomain\tfamily\tintent\tknown_limit\texpected_path\tstatus\tmin_tier\tcurrent_tier\tmilestone\trationale".to_owned(),
        ];

        for (index, entry) in self.ranked.iter().enumerate() {
            output.push(format!(
                "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
                index + 1,
                entry.row.corpus.as_str(),
                entry.row.domain,
                entry.row.family,
                entry.row.intent.as_deref().unwrap_or("-"),
                entry.row.known_limit,
                entry.row.expected_path.as_str(),
                entry.row.status.as_str(),
                entry.row.min_tier,
                entry.row.current_tier,
                entry.milestone.as_str(),
                entry.rationale,
            ));
        }

        output.join("\n")
    }
}

fn run_compatibility_backlog_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let (mut top100_path, mut forum_path) = default_compatibility_slo_paths();
    let mut top_n = COMPATIBILITY_BACKLOG_TOP_N_DEFAULT;

    while let Some(argument) = args.next() {
        match argument.as_ref() {
            "--top100" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                top100_path = path.as_ref().to_owned();
            }
            "--forum" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                forum_path = path.as_ref().to_owned();
            }
            "--top" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                top_n = value
                    .as_ref()
                    .parse::<usize>()
                    .map_err(|error| format!("invalid --top value {}: {error}", value.as_ref()))?;
                if top_n == 0 {
                    return Err("invalid --top value 0: must be > 0".to_owned());
                }
            }
            other => return Err(format!("unsupported compatibility-backlog option: {other}")),
        }
    }

    let report = compatibility_backlog_report_from_paths(&top100_path, &forum_path, top_n)?;
    Ok(CliAction::Print(report.to_text()))
}

fn compatibility_backlog_report_from_paths(
    top100_path: &str,
    forum_path: &str,
    top_n: usize,
) -> Result<CompatibilityBacklogReport, String> {
    let top100_text = fs::read_to_string(top100_path)
        .map_err(|error| format!("failed to read {top100_path}: {error}"))?;
    let forum_text = fs::read_to_string(forum_path)
        .map_err(|error| format!("failed to read {forum_path}: {error}"))?;
    let top100_rows = parse_top100_backlog_rows(&top100_text)?;
    let forum_rows = parse_forum_backlog_rows(&forum_text)?;

    Ok(evaluate_compatibility_backlog(
        top100_path,
        forum_path,
        top_n,
        &top100_rows,
        &forum_rows,
    ))
}

fn evaluate_compatibility_backlog(
    top100_path: &str,
    forum_path: &str,
    top_n: usize,
    top100_rows: &[CompatibilityBacklogRow],
    forum_rows: &[CompatibilityBacklogRow],
) -> CompatibilityBacklogReport {
    let mut rows = Vec::new();
    rows.extend_from_slice(top100_rows);
    rows.extend_from_slice(forum_rows);
    rows.retain(|row| {
        !matches!(
            row.status,
            CoverageStatus::Covered | CoverageStatus::Blocked
        )
    });
    rows.sort_by(|a, b| a.priority_key().cmp(&b.priority_key()));
    let rows_total = rows.len();

    let ranked = rows
        .into_iter()
        .take(top_n)
        .map(|row| {
            let (milestone, rationale) = map_backlog_row_to_milestone(&row);
            CompatibilityBacklogEntry {
                row,
                milestone,
                rationale,
            }
        })
        .collect::<Vec<_>>();

    CompatibilityBacklogReport {
        top100_path: top100_path.to_owned(),
        forum_path: forum_path.to_owned(),
        top_n,
        rows_total,
        ranked,
    }
}

fn map_backlog_row_to_milestone(row: &CompatibilityBacklogRow) -> (CompatibilityMilestone, String) {
    let family = canonical_family_name(&row.family);

    if row.expected_path == CoverageExpectedPath::Failure || row.known_limit != "none" {
        return (
            CompatibilityMilestone::M76,
            "blocked-flow or failure-quality improvement".to_owned(),
        );
    }

    if let Some(intent) = row.intent.as_deref() {
        match intent {
            "article-or-reference" | "portal-landing" => {
                return (
                    CompatibilityMilestone::M74,
                    "generic readability and content selection".to_owned(),
                );
            }
            "search-results" | "feed-or-thread" | "marketplace-listing" | "video-hub" => {
                return (
                    CompatibilityMilestone::M75,
                    "generic actionability for lists, links, and flows".to_owned(),
                );
            }
            "app-shell" => {
                return (
                    CompatibilityMilestone::M78,
                    "family-pack expansion for app-shell structures".to_owned(),
                );
            }
            _ => {}
        }
    }

    if family.contains("forum")
        || family.contains("stackexchange")
        || family.contains("social-community")
        || family.contains("xenforo")
    {
        return (
            CompatibilityMilestone::M75,
            "thread/list actionability for community pages".to_owned(),
        );
    }

    (
        CompatibilityMilestone::M74,
        "generic readability baseline lift".to_owned(),
    )
}

fn parse_top100_backlog_rows(text: &str) -> Result<Vec<CompatibilityBacklogRow>, String> {
    let mut rows = Vec::new();
    for (line_number, line) in text.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let fields = trimmed.split('\t').collect::<Vec<_>>();
        if fields.len() < 9 {
            return Err(format!(
                "invalid top100 backlog row at line {}: expected 9 fields, got {}",
                line_number + 1,
                fields.len()
            ));
        }
        let domain = fields[0].trim().to_owned();
        let family = canonical_family_name(fields[1]);
        let intent = fields[2].trim().to_owned();
        let context = format!("top100 row {} ({domain})", line_number + 1);
        let min_tier = parse_slo_tier(fields[3], &context)?;
        let current_tier = parse_slo_tier(fields[4], &context)?;
        let expected_path = CoverageExpectedPath::parse(fields[6], &context)?;
        let status = CoverageStatus::parse(fields[7], &context)?;
        let known_limit = fields[8].trim().to_owned();
        if family.is_empty() || intent.is_empty() || known_limit.is_empty() {
            return Err(format!("missing required backlog fields for {context}"));
        }
        rows.push(CompatibilityBacklogRow {
            corpus: CompatibilityCorpus::Top100,
            domain,
            family,
            intent: Some(intent),
            min_tier,
            current_tier,
            expected_path,
            status,
            known_limit,
        });
    }
    Ok(rows)
}

fn parse_forum_backlog_rows(text: &str) -> Result<Vec<CompatibilityBacklogRow>, String> {
    let mut rows = Vec::new();
    for (line_number, line) in text.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let fields = trimmed.split('\t').collect::<Vec<_>>();
        if fields.len() < 8 {
            return Err(format!(
                "invalid forum backlog row at line {}: expected 8 fields, got {}",
                line_number + 1,
                fields.len()
            ));
        }
        let domain = fields[0].trim().to_owned();
        let family = canonical_family_name(fields[1]);
        let context = format!("forum row {} ({domain})", line_number + 1);
        let min_tier = parse_slo_tier(fields[2], &context)?;
        let current_tier = parse_slo_tier(fields[3], &context)?;
        let expected_path = CoverageExpectedPath::parse(fields[5], &context)?;
        let status = CoverageStatus::parse(fields[6], &context)?;
        let known_limit = fields[7].trim().to_owned();
        if family.is_empty() || known_limit.is_empty() {
            return Err(format!("missing required backlog fields for {context}"));
        }
        rows.push(CompatibilityBacklogRow {
            corpus: CompatibilityCorpus::Forum,
            domain,
            family,
            intent: None,
            min_tier,
            current_tier,
            expected_path,
            status,
            known_limit,
        });
    }
    Ok(rows)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LiveOutcome {
    Ok,
    Timeout,
    TransientError,
    Blocked,
}

impl LiveOutcome {
    fn parse(input: &str) -> Option<Self> {
        match input.trim().to_ascii_lowercase().as_str() {
            "ok" => Some(Self::Ok),
            "timeout" => Some(Self::Timeout),
            "transient-error" | "transient_error" | "transient" => Some(Self::TransientError),
            "blocked" => Some(Self::Blocked),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct LiveVarianceTarget {
    origin: String,
    family: String,
    enabled: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct LiveVarianceRow {
    timestamp_utc: String,
    origin: String,
    outcome: LiveOutcome,
    latency_ms: Option<u64>,
    blocked_class: Option<String>,
}

#[derive(Debug, Clone, PartialEq)]
struct LiveVarianceOriginStats {
    origin: String,
    family: String,
    samples: usize,
    ok: usize,
    timeout: usize,
    transient_error: usize,
    blocked: usize,
    flake_rate: f64,
    timeout_rate: f64,
    recent_flake_rate: f64,
    previous_flake_rate: f64,
    trend_delta: f64,
}

#[derive(Debug, Clone, PartialEq)]
struct LiveVarianceReport {
    targets_path: String,
    runs_path: String,
    window: usize,
    rows_total: usize,
    enabled_targets: usize,
    total_samples: usize,
    global_flake_rate: f64,
    global_timeout_rate: f64,
    origins: Vec<LiveVarianceOriginStats>,
    blocked_class_counts: Vec<(String, usize)>,
}

impl LiveVarianceReport {
    fn to_text(&self) -> String {
        let mut output = vec![
            "index-compatibility-live-variance-v1".to_owned(),
            format!("targets: {}", self.targets_path),
            format!("runs: {}", self.runs_path),
            format!("window: {}", self.window),
            format!("rows_total: {}", self.rows_total),
            format!("enabled_targets: {}", self.enabled_targets),
            format!("samples_total: {}", self.total_samples),
            format!(
                "global_rates: flake={:.4} timeout={:.4}",
                self.global_flake_rate, self.global_timeout_rate
            ),
            "origin_columns: origin\tfamily\tsamples\tok\ttimeout\ttransient_error\tblocked\tflake_rate\ttimeout_rate\trecent_flake\tprevious_flake\ttrend_delta".to_owned(),
        ];
        for origin in &self.origins {
            output.push(format!(
                "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{:.4}\t{:.4}\t{:.4}\t{:.4}\t{:+.4}",
                origin.origin,
                origin.family,
                origin.samples,
                origin.ok,
                origin.timeout,
                origin.transient_error,
                origin.blocked,
                origin.flake_rate,
                origin.timeout_rate,
                origin.recent_flake_rate,
                origin.previous_flake_rate,
                origin.trend_delta,
            ));
        }
        if self.blocked_class_counts.is_empty() {
            output.push("blocked_classes: none".to_owned());
        } else {
            output.push("blocked_class_columns: class\tcount".to_owned());
            for (class_name, count) in &self.blocked_class_counts {
                output.push(format!("{class_name}\t{count}"));
            }
        }
        output.push("result: pass".to_owned());
        output.join("\n")
    }
}

fn default_live_variance_paths() -> (String, String) {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
    (
        root.join(LIVE_VARIANCE_TARGETS_PATH).display().to_string(),
        root.join(LIVE_VARIANCE_RUNS_PATH).display().to_string(),
    )
}

fn run_compatibility_live_variance_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let (mut targets_path, mut runs_path) = default_live_variance_paths();
    let mut window = LIVE_VARIANCE_WINDOW_DEFAULT;
    while let Some(argument) = args.next() {
        match argument.as_ref() {
            "--targets" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                targets_path = path.as_ref().to_owned();
            }
            "--runs" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                runs_path = path.as_ref().to_owned();
            }
            "--window" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                window = value.as_ref().parse::<usize>().map_err(|error| {
                    format!("invalid --window value {}: {error}", value.as_ref())
                })?;
                if window == 0 || window > LIVE_VARIANCE_MAX_WINDOW {
                    return Err(format!(
                        "invalid --window value {window}: expected 1..={LIVE_VARIANCE_MAX_WINDOW}"
                    ));
                }
            }
            other => {
                return Err(format!(
                    "unsupported compatibility-live-variance option: {other}"
                ));
            }
        }
    }
    let report = compatibility_live_variance_report_from_paths(&targets_path, &runs_path, window)?;
    Ok(CliAction::Print(report.to_text()))
}

fn compatibility_live_variance_report_from_paths(
    targets_path: &str,
    runs_path: &str,
    window: usize,
) -> Result<LiveVarianceReport, String> {
    let targets_text = fs::read_to_string(targets_path)
        .map_err(|error| format!("failed to read {targets_path}: {error}"))?;
    let runs_text = fs::read_to_string(runs_path)
        .map_err(|error| format!("failed to read {runs_path}: {error}"))?;
    let targets = parse_live_variance_targets(&targets_text)?;
    let rows = parse_live_variance_rows(&runs_text)?;
    Ok(evaluate_live_variance_report(
        targets_path,
        runs_path,
        &targets,
        &rows,
        window,
    ))
}

fn evaluate_live_variance_report(
    targets_path: &str,
    runs_path: &str,
    targets: &[LiveVarianceTarget],
    rows: &[LiveVarianceRow],
    window: usize,
) -> LiveVarianceReport {
    let mut families = BTreeMap::new();
    let mut enabled = BTreeMap::new();
    for target in targets {
        families.insert(target.origin.clone(), target.family.clone());
        enabled.insert(target.origin.clone(), target.enabled);
    }

    let filtered = rows
        .iter()
        .filter(|row| enabled.get(&row.origin).copied().unwrap_or(true))
        .cloned()
        .collect::<Vec<_>>();

    let mut rows_by_origin = BTreeMap::<String, Vec<LiveVarianceRow>>::new();
    let mut blocked_counts = BTreeMap::<String, usize>::new();
    let mut totals = (0usize, 0usize, 0usize);
    for row in filtered {
        totals.0 = totals.0.saturating_add(1);
        if !matches!(row.outcome, LiveOutcome::Ok) {
            totals.1 = totals.1.saturating_add(1);
        }
        if matches!(row.outcome, LiveOutcome::Timeout) {
            totals.2 = totals.2.saturating_add(1);
        }
        if let Some(class_name) = row.blocked_class.as_ref() {
            let counter = blocked_counts.entry(class_name.clone()).or_default();
            *counter = counter.saturating_add(1);
        }
        rows_by_origin
            .entry(row.origin.clone())
            .or_default()
            .push(row);
    }

    let origins = rows_by_origin
        .into_iter()
        .map(|(origin, entries)| {
            let samples = entries.len();
            let ok = entries
                .iter()
                .filter(|row| matches!(row.outcome, LiveOutcome::Ok))
                .count();
            let timeout = entries
                .iter()
                .filter(|row| matches!(row.outcome, LiveOutcome::Timeout))
                .count();
            let transient_error = entries
                .iter()
                .filter(|row| matches!(row.outcome, LiveOutcome::TransientError))
                .count();
            let blocked = entries
                .iter()
                .filter(|row| matches!(row.outcome, LiveOutcome::Blocked))
                .count();

            let flake = samples.saturating_sub(ok);
            let flake_rate = live_ratio(flake, samples);
            let timeout_rate = live_ratio(timeout, samples);

            let recent = entries.iter().rev().take(window).collect::<Vec<_>>();
            let previous = entries
                .iter()
                .rev()
                .skip(window)
                .take(window)
                .collect::<Vec<_>>();
            let recent_flake = recent
                .iter()
                .filter(|row| !matches!(row.outcome, LiveOutcome::Ok))
                .count();
            let previous_flake = previous
                .iter()
                .filter(|row| !matches!(row.outcome, LiveOutcome::Ok))
                .count();
            let recent_flake_rate = live_ratio(recent_flake, recent.len());
            let previous_flake_rate = live_ratio(previous_flake, previous.len());
            let trend_delta = recent_flake_rate - previous_flake_rate;

            LiveVarianceOriginStats {
                origin: origin.clone(),
                family: families
                    .get(&origin)
                    .cloned()
                    .unwrap_or_else(|| "unknown".to_owned()),
                samples,
                ok,
                timeout,
                transient_error,
                blocked,
                flake_rate,
                timeout_rate,
                recent_flake_rate,
                previous_flake_rate,
                trend_delta,
            }
        })
        .collect::<Vec<_>>();

    LiveVarianceReport {
        targets_path: targets_path.to_owned(),
        runs_path: runs_path.to_owned(),
        window,
        rows_total: rows.len(),
        enabled_targets: targets.iter().filter(|target| target.enabled).count(),
        total_samples: totals.0,
        global_flake_rate: live_ratio(totals.1, totals.0),
        global_timeout_rate: live_ratio(totals.2, totals.0),
        origins,
        blocked_class_counts: blocked_counts.into_iter().collect(),
    }
}

fn parse_live_variance_targets(input: &str) -> Result<Vec<LiveVarianceTarget>, String> {
    let mut rows = Vec::new();
    for (line_number, line) in input.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let fields = trimmed.split('\t').collect::<Vec<_>>();
        if fields.len() < 2 {
            return Err(format!(
                "invalid live-variance target row at line {}: expected at least 2 fields",
                line_number + 1
            ));
        }
        let origin = fields[0].trim().to_ascii_lowercase();
        if origin.is_empty() {
            return Err(format!(
                "invalid live-variance target row at line {}: origin is empty",
                line_number + 1
            ));
        }
        let family = fields[1].trim().to_owned();
        let enabled = fields
            .get(2)
            .map(|value| match value.trim().to_ascii_lowercase().as_str() {
                "0" | "false" | "no" => Ok(false),
                "1" | "true" | "yes" | "" => Ok(true),
                other => Err(format!(
                    "invalid enabled flag at line {}: {}",
                    line_number + 1,
                    other
                )),
            })
            .unwrap_or(Ok(true))?;
        rows.push(LiveVarianceTarget {
            origin,
            family,
            enabled,
        });
    }
    rows.sort_by(|left, right| left.origin.cmp(&right.origin));
    Ok(rows)
}

fn parse_live_variance_rows(input: &str) -> Result<Vec<LiveVarianceRow>, String> {
    let mut rows = Vec::new();
    for (line_number, line) in input.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let fields = trimmed.split('\t').collect::<Vec<_>>();
        if fields.len() < 3 {
            return Err(format!(
                "invalid live-variance row at line {}: expected at least 3 fields",
                line_number + 1
            ));
        }
        let timestamp_utc = fields[0].trim().to_owned();
        let origin = fields[1].trim().to_ascii_lowercase();
        let Some(outcome) = LiveOutcome::parse(fields[2]) else {
            return Err(format!(
                "invalid live-variance outcome at line {}: {}",
                line_number + 1,
                fields[2].trim()
            ));
        };
        let latency_ms = fields
            .get(3)
            .map(|value| value.trim())
            .filter(|value| !value.is_empty() && *value != "-")
            .map(|value| {
                value.parse::<u64>().map_err(|error| {
                    format!(
                        "invalid live-variance latency at line {}: {}",
                        line_number + 1,
                        error
                    )
                })
            })
            .transpose()?;
        let blocked_class = fields
            .get(4)
            .map(|value| value.trim())
            .filter(|value| !value.is_empty() && *value != "-")
            .map(ToOwned::to_owned);
        if origin.is_empty() || timestamp_utc.is_empty() {
            return Err(format!(
                "invalid live-variance row at line {}: missing timestamp or origin",
                line_number + 1
            ));
        }
        rows.push(LiveVarianceRow {
            timestamp_utc,
            origin,
            outcome,
            latency_ms,
            blocked_class,
        });
    }
    rows.sort_by(|left, right| {
        (left.origin.as_str(), left.timestamp_utc.as_str())
            .cmp(&(right.origin.as_str(), right.timestamp_utc.as_str()))
    });
    Ok(rows)
}

fn live_ratio(numerator: usize, denominator: usize) -> f64 {
    if denominator == 0 {
        0.0
    } else {
        numerator as f64 / denominator as f64
    }
}

fn run_compatibility_recovery_plan_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(target) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    if args.next().is_some() {
        return Err("too many arguments".to_owned());
    }
    let normalized = normalize_url_input(target.as_ref());
    let profile = recovery_profile_for_target(&normalized);
    let mut output = vec![
        "index-compatibility-recovery-plan-v2".to_owned(),
        format!("target: {normalized}"),
        format!("profile: {}", profile.as_str()),
        format!(
            "fallback_order: {}",
            recovery_fallback_order(profile).join(" -> ")
        ),
        "stage_timeout_columns: stage\tseconds".to_owned(),
    ];
    for stage in [
        RuntimeStage::Fetching,
        RuntimeStage::Snapshotting,
        RuntimeStage::Parsing,
        RuntimeStage::Transforming,
        RuntimeStage::Scoring,
        RuntimeStage::Storing,
    ] {
        let seconds = stage_timeout_for_target(stage, &normalized)
            .map(|value| value.as_secs().to_string())
            .unwrap_or_else(|| "-".to_owned());
        output.push(format!("{}\t{}", stage.as_str(), seconds));
    }
    output.push("result: pass".to_owned());
    Ok(CliAction::Print(output.join("\n")))
}

fn auth_cookie_store_path() -> PathBuf {
    Path::new(&runtime_paths().state_dir)
        .join("auth")
        .join("cookies.store")
}

fn run_auth_assist_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(command) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    match command.as_ref() {
        "inspect" => {
            let Some(target) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            Ok(CliAction::Print(auth_assist_inspect(target.as_ref())?))
        }
        "import" => {
            let Some(path) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            Ok(CliAction::Print(auth_assist_import(path.as_ref())?))
        }
        "export" => {
            let Some(path) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            Ok(CliAction::Print(auth_assist_export(path.as_ref())?))
        }
        "diagnose-submit" => {
            let Some(url) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(status) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let status_code = status
                .as_ref()
                .parse::<u16>()
                .map_err(|error| format!("invalid status code {}: {error}", status.as_ref()))?;
            let message = args
                .map(|part| part.as_ref().to_owned())
                .collect::<Vec<_>>();
            if message.is_empty() {
                return Ok(CliAction::Help(help()));
            }
            Ok(CliAction::Print(auth_assist_diagnose_submit(
                url.as_ref(),
                status_code,
                &message.join(" "),
            )))
        }
        other => Err(format!("unsupported auth-assist command: {other}")),
    }
}

fn auth_assist_import(path: &str) -> Result<String, String> {
    let bytes = fs::read(path).map_err(|error| format!("failed to read {path}: {error}"))?;
    let mut storage = MemorySecureStorage::new();
    storage
        .store("cookies", &bytes)
        .map_err(|error| format!("failed to stage cookies: {error}"))?;
    let jar = CookieJar::load(&storage, "cookies")
        .map_err(|error| format!("invalid cookie bundle: {error}"))?;
    let normalized = jar
        .header_for(&IndexUrl::parse("https://example.org").map_err(|error| error.to_string())?)
        .unwrap_or_default();
    let target = auth_cookie_store_path();
    if let Some(parent) = target.parent() {
        fs::create_dir_all(parent)
            .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
    }
    fs::write(&target, &bytes)
        .map_err(|error| format!("failed to write {}: {error}", target.display()))?;
    Ok(format!(
        "index-auth-assist-import-v1\nsource: {path}\nstored_to: {}\nprobe_cookie_header_len: {}\nresult: pass",
        target.display(),
        normalized.len()
    ))
}

fn auth_assist_export(path: &str) -> Result<String, String> {
    let source = auth_cookie_store_path();
    let bytes = fs::read(&source)
        .map_err(|error| format!("failed to read {}: {error}", source.display()))?;
    fs::write(path, bytes).map_err(|error| format!("failed to write {path}: {error}"))?;
    Ok(format!(
        "index-auth-assist-export-v1\nsource: {}\noutput: {path}\nresult: pass",
        source.display()
    ))
}

fn auth_assist_inspect(target: &str) -> Result<String, String> {
    let normalized = normalize_url_input(target);
    let url = IndexUrl::parse(&normalized).map_err(|error| error.to_string())?;
    let source = auth_cookie_store_path();
    let bytes = match fs::read(&source) {
        Ok(bytes) => bytes,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            return Ok(format!(
                "index-auth-assist-inspect-v1\ntarget: {normalized}\nstore: {}\nsession: missing\nresult: pass",
                source.display()
            ));
        }
        Err(error) => {
            return Err(format!(
                "failed to read auth cookie store {}: {error}",
                source.display()
            ));
        }
    };
    let mut storage = MemorySecureStorage::new();
    storage
        .store("cookies", &bytes)
        .map_err(|error| format!("failed to stage cookies: {error}"))?;
    let jar = CookieJar::load(&storage, "cookies")
        .map_err(|error| format!("invalid cookie bundle: {error}"))?;
    let header = jar.header_for(&url);
    let names = header
        .as_deref()
        .map(cookie_header_names)
        .unwrap_or_default()
        .join(", ");
    Ok(format!(
        "index-auth-assist-inspect-v1\ntarget: {normalized}\nstore: {}\norigin_cookie_names: {}\nresult: pass",
        source.display(),
        if names.is_empty() {
            "-".to_owned()
        } else {
            names
        }
    ))
}

fn cookie_header_names(header: &str) -> Vec<String> {
    let mut names = header
        .split(';')
        .filter_map(|part| part.split_once('='))
        .map(|(name, _)| name.trim().to_owned())
        .filter(|name| !name.is_empty())
        .collect::<Vec<_>>();
    names.sort();
    names.dedup();
    names
}

fn auth_assist_diagnose_submit(url: &str, status: u16, message: &str) -> String {
    let normalized = normalize_url_input(url);
    let lower = message.to_ascii_lowercase();
    let (class, remediation) = if lower.contains("csrf")
        || lower.contains("xsrf")
        || (lower.contains("token") && matches!(status, 400 | 401 | 403 | 419))
    {
        (
            "csrf-token-mismatch",
            "refresh session cookies and resubmit with updated form token",
        )
    } else if matches!(status, 419 | 440) || lower.contains("expired session") {
        (
            "session-expired",
            "re-authenticate and retry the form submission",
        )
    } else if status == 401 {
        (
            "auth-required",
            "authenticate in browser context and import local session cookies",
        )
    } else if status == 403 {
        (
            "origin-policy-denied",
            "check origin/session scope and token freshness",
        )
    } else {
        (
            "generic-submit-failure",
            "capture the response and inspect form field mappings",
        )
    };
    format!(
        "index-auth-assist-diagnose-v1\ntarget: {normalized}\nstatus: {status}\nclass: {class}\nmessage: {message}\nremediation: {remediation}\nresult: pass"
    )
}

fn run_challenge_diagnose_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(target) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    let remaining = args
        .map(|part| part.as_ref().to_owned())
        .collect::<Vec<_>>();
    if remaining.is_empty() {
        return Ok(CliAction::Help(help()));
    }
    let first = &remaining[0];
    let text = match fs::read_to_string(first) {
        Ok(contents) => contents,
        Err(_) => remaining.join(" "),
    };
    let report = challenge_diagnose_report(target.as_ref(), &text);
    Ok(CliAction::Print(report))
}

fn challenge_diagnose_report(target: &str, body: &str) -> String {
    let normalized = normalize_url_input(target);
    let lower = body.to_ascii_lowercase();
    let (class_name, reason) = if lower.contains("captcha") {
        ("captcha", "captcha challenge detected")
    } else if lower.contains("verify you are human")
        || lower.contains("cloudflare")
        || lower.contains("attention required")
        || lower.contains("bot detection")
    {
        ("bot-gate", "anti-bot challenge detected")
    } else if lower.contains("not available in your region")
        || lower.contains("geo-blocked")
        || lower.contains("country not supported")
    {
        ("geo-gate", "geo restriction detected")
    } else if lower.contains("age verification")
        || lower.contains("confirm your age")
        || lower.contains("you must be 18")
    {
        ("age-gate", "age-gated workflow detected")
    } else if lower.contains("sign in")
        || lower.contains("log in")
        || lower.contains("login required")
    {
        ("auth-wall", "authentication gate detected")
    } else if lower.contains("access denied") || lower.contains("blocked by policy") {
        ("policy-blocked", "policy or access control gate detected")
    } else {
        ("none", "no strong challenge marker detected")
    };
    format!(
        "index-challenge-diagnose-v1\ntarget: {normalized}\nclass: {class_name}\nreason: {reason}\nresult: pass"
    )
}

#[derive(Debug, Clone, PartialEq)]
struct CompatibilityRecoveryGateReport {
    top100_path: String,
    forum_path: String,
    live_targets_path: String,
    live_runs_path: String,
    live_window: usize,
    max_live_flake_rate: f64,
    max_live_timeout_rate: f64,
    slo_v2_passed: bool,
    live_flake_rate: f64,
    live_timeout_rate: f64,
    violations: Vec<String>,
}

impl CompatibilityRecoveryGateReport {
    fn passed(&self) -> bool {
        self.violations.is_empty()
    }

    fn to_text(&self) -> String {
        let mut output = vec![
            "index-compatibility-recovery-gate-v1".to_owned(),
            format!("top100_matrix: {}", self.top100_path),
            format!("forum_matrix: {}", self.forum_path),
            format!("live_targets: {}", self.live_targets_path),
            format!("live_runs: {}", self.live_runs_path),
            format!("live_window: {}", self.live_window),
            format!(
                "thresholds: live_flake<={:.4} live_timeout<={:.4}",
                self.max_live_flake_rate, self.max_live_timeout_rate
            ),
            format!("slo_v2_passed: {}", self.slo_v2_passed),
            format!(
                "live_rates: flake={:.4} timeout={:.4}",
                self.live_flake_rate, self.live_timeout_rate
            ),
        ];
        if self.violations.is_empty() {
            output.push("violations: none".to_owned());
        } else {
            output.push("violations:".to_owned());
            for violation in &self.violations {
                output.push(format!("- {violation}"));
            }
        }
        output.push(format!(
            "result: {}",
            if self.passed() { "pass" } else { "fail" }
        ));
        output.join("\n")
    }
}

fn run_compatibility_recovery_gate_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let (mut top100_path, mut forum_path) = default_compatibility_slo_paths();
    let (mut live_targets_path, mut live_runs_path) = default_live_variance_paths();
    let mut thresholds = CompatibilitySloThresholds::default();
    let mut live_window = LIVE_VARIANCE_WINDOW_DEFAULT;
    let mut max_live_flake_rate = 0.35_f64;
    let mut max_live_timeout_rate = 0.20_f64;

    while let Some(argument) = args.next() {
        match argument.as_ref() {
            "--top100" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                top100_path = path.as_ref().to_owned();
            }
            "--forum" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                forum_path = path.as_ref().to_owned();
            }
            "--live-targets" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                live_targets_path = path.as_ref().to_owned();
            }
            "--live-runs" => {
                let Some(path) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                live_runs_path = path.as_ref().to_owned();
            }
            "--live-window" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                live_window = value.as_ref().parse::<usize>().map_err(|error| {
                    format!("invalid --live-window value {}: {error}", value.as_ref())
                })?;
                if live_window == 0 || live_window > LIVE_VARIANCE_MAX_WINDOW {
                    return Err(format!(
                        "invalid --live-window value {live_window}: expected 1..={LIVE_VARIANCE_MAX_WINDOW}"
                    ));
                }
            }
            "--max-live-flake-rate" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                max_live_flake_rate = parse_slo_floor("max-live-flake-rate", value.as_ref())?;
            }
            "--max-live-timeout-rate" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                max_live_timeout_rate = parse_slo_floor("max-live-timeout-rate", value.as_ref())?;
            }
            "--min-readability" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.readability_floor = parse_slo_floor("readability", value.as_ref())?;
            }
            "--min-actionability" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.actionability_floor = parse_slo_floor("actionability", value.as_ref())?;
            }
            "--min-failure-quality" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                thresholds.failure_quality_floor =
                    parse_slo_floor("failure-quality", value.as_ref())?;
            }
            other => {
                return Err(format!(
                    "unsupported compatibility-recovery-gate option: {other}"
                ));
            }
        }
    }

    let slo = compatibility_slo_v2_report_from_paths(
        &top100_path,
        &forum_path,
        thresholds,
        Some(&top100_path),
        Some(&forum_path),
    )?;
    let live = compatibility_live_variance_report_from_paths(
        &live_targets_path,
        &live_runs_path,
        live_window,
    )?;

    let mut violations = Vec::new();
    if !slo.passed() {
        violations.push("compatibility-slo-v2 gate failed".to_owned());
    }
    if live.global_flake_rate > max_live_flake_rate {
        violations.push(format!(
            "live flake rate {:.4} exceeds threshold {:.4}",
            live.global_flake_rate, max_live_flake_rate
        ));
    }
    if live.global_timeout_rate > max_live_timeout_rate {
        violations.push(format!(
            "live timeout rate {:.4} exceeds threshold {:.4}",
            live.global_timeout_rate, max_live_timeout_rate
        ));
    }

    let report = CompatibilityRecoveryGateReport {
        top100_path,
        forum_path,
        live_targets_path,
        live_runs_path,
        live_window,
        max_live_flake_rate,
        max_live_timeout_rate,
        slo_v2_passed: slo.passed(),
        live_flake_rate: live.global_flake_rate,
        live_timeout_rate: live.global_timeout_rate,
        violations,
    };

    if report.passed() {
        Ok(CliAction::Print(report.to_text()))
    } else {
        Err(report.to_text())
    }
}

fn run_compatibility_pack_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(command) = args.next() else {
        return Ok(CliAction::Help(help()));
    };

    match command.as_ref() {
        "lint" => {
            let Some(pack_path) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(page_url) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let report = lint_pack_file(pack_path.as_ref(), page_url.as_ref())?;
            Ok(CliAction::Print(report))
        }
        "inspect" => {
            let Some(page_url) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let paths = runtime_paths();
            let report = inspect_runtime_for_url(&paths.config_dir, page_url.as_ref())?;
            Ok(CliAction::Print(report))
        }
        "install" => {
            let Some(pack_path) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let mut source = PackSource::User;
            if let Some(option) = args.next() {
                source = match option.as_ref() {
                    "--trusted" => PackSource::Trusted,
                    "--user" => PackSource::User,
                    other => {
                        return Err(format!(
                            "unsupported compatibility-pack install option: {other}"
                        ));
                    }
                };
                if args.next().is_some() {
                    return Err("too many arguments".to_owned());
                }
            }
            let paths = runtime_paths();
            let target = install_pack_file(&paths.config_dir, pack_path.as_ref(), source)?;
            Ok(CliAction::Print(format!(
                "index-compat-pack-install-v1\nsource: {}\npack: {}\ninstalled_to: {}\nresult: pass",
                source.as_str(),
                pack_path.as_ref(),
                target.display()
            )))
        }
        "update" => {
            let Some(pack_id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(pack_path) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let mut source = PackSource::User;
            if let Some(option) = args.next() {
                source = match option.as_ref() {
                    "--trusted" => PackSource::Trusted,
                    "--user" => PackSource::User,
                    other => {
                        return Err(format!(
                            "unsupported compatibility-pack update option: {other}"
                        ));
                    }
                };
                if args.next().is_some() {
                    return Err("too many arguments".to_owned());
                }
            }
            let paths = runtime_paths();
            let target = install_pack_file(&paths.config_dir, pack_path.as_ref(), source)?;
            let expected_name = format!("{}.pack.json", pack_id.as_ref());
            let actual_name = target
                .file_name()
                .and_then(|value| value.to_str())
                .unwrap_or_default()
                .to_owned();
            if actual_name != expected_name {
                return Err(format!(
                    "pack id mismatch: expected {}, installed {}",
                    pack_id.as_ref(),
                    actual_name
                ));
            }
            Ok(CliAction::Print(format!(
                "index-compat-pack-update-v1\nsource: {}\npack_id: {}\npack: {}\nupdated_to: {}\nresult: pass",
                source.as_str(),
                pack_id.as_ref(),
                pack_path.as_ref(),
                target.display()
            )))
        }
        "list" => {
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let paths = runtime_paths();
            let files = list_runtime_pack_files(&paths.config_dir);
            let mut output = vec![
                "index-compat-pack-list-v1".to_owned(),
                format!("count: {}", files.len()),
                "columns: source\tpath".to_owned(),
            ];
            output.extend(files);
            output.push("result: pass".to_owned());
            Ok(CliAction::Print(output.join("\n")))
        }
        "remove" => {
            let Some(pack_id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let paths = runtime_paths();
            let removed = remove_compatibility_pack(&paths.config_dir, pack_id.as_ref())?;
            Ok(CliAction::Print(format!(
                "index-compat-pack-remove-v1\npack_id: {}\nremoved_files: {}\nresult: pass",
                pack_id.as_ref(),
                removed
            )))
        }
        "rollback" => {
            let Some(pack_id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let paths = runtime_paths();
            let restored = rollback_pack_file(&paths.config_dir, pack_id.as_ref())?;
            Ok(CliAction::Print(format!(
                "index-compat-pack-rollback-v1\npack_id: {}\nrestored_to: {}\nresult: pass",
                pack_id.as_ref(),
                restored.display()
            )))
        }
        "sign" => {
            let Some(pack_path) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(key_id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(secret) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let bytes = fs::read(pack_path.as_ref())
                .map_err(|error| format!("failed to read {}: {error}", pack_path.as_ref()))?;
            let signature = sign_pack_bytes(&bytes, key_id.as_ref(), secret.as_ref());
            Ok(CliAction::Print(format!(
                "index-compat-pack-sign-v1\npack: {}\nkey_id: {}\nsignature: {}\nresult: pass",
                pack_path.as_ref(),
                key_id.as_ref(),
                signature
            )))
        }
        "verify" => {
            let Some(pack_path) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(key_id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(secret) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(signature) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let bytes = fs::read(pack_path.as_ref())
                .map_err(|error| format!("failed to read {}: {error}", pack_path.as_ref()))?;
            if verify_pack_signature(&bytes, key_id.as_ref(), secret.as_ref(), signature.as_ref()) {
                Ok(CliAction::Print(format!(
                    "index-compat-pack-verify-v1\npack: {}\nkey_id: {}\nresult: pass",
                    pack_path.as_ref(),
                    key_id.as_ref()
                )))
            } else {
                Err(format!(
                    "index-compat-pack-verify-v1\npack: {}\nkey_id: {}\nresult: fail\nerror: signature mismatch",
                    pack_path.as_ref(),
                    key_id.as_ref()
                ))
            }
        }
        other => Err(format!("unsupported compatibility-pack command: {other}")),
    }
}

fn remove_compatibility_pack(config_dir: &str, pack_id: &str) -> Result<usize, String> {
    let user_path = Path::new(config_dir)
        .join("compat-packs/user")
        .join(format!("{pack_id}.pack.json"));
    let trusted_path = Path::new(config_dir)
        .join("compat-packs/trusted")
        .join(format!("{pack_id}.pack.json"));
    let mut removed = 0usize;
    for path in [user_path, trusted_path] {
        match fs::remove_file(&path) {
            Ok(()) => {
                removed += 1;
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => {
                return Err(format!(
                    "failed to remove compatibility pack {}: {error}",
                    path.display()
                ));
            }
        }
    }
    Ok(removed)
}

fn run_adapter_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(command) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    match command.as_ref() {
        "check" => {
            let Some(fixture) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            Ok(CliAction::Print(adapter_check_report(fixture.as_ref())?))
        }
        "scaffold" => {
            let Some(fixture) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            Ok(CliAction::Print(adapter_manifest_scaffold(
                fixture.as_ref(),
            )?))
        }
        "diff" => {
            let Some(fixture) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(manifest) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            Ok(CliAction::Print(adapter_manifest_diff(
                fixture.as_ref(),
                manifest.as_ref(),
            )?))
        }
        other => Err(format!("unsupported adapter command: {other}")),
    }
}

fn run_idx_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(command) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    match command.as_ref() {
        "lint" => run_idx_lint_command(args),
        other => Err(format!("unsupported idx command: {other}")),
    }
}

fn run_idx_lint_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(manifest_path) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    let manifest_path = manifest_path.as_ref();
    let Some(page_url) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    let page_url = page_url.as_ref();
    let mut source_url = well_known_index_manifest_url(page_url)
        .ok_or_else(|| format!("invalid page URL for idx lint: {page_url}"))?;

    while let Some(argument) = args.next() {
        match argument.as_ref() {
            "--source-url" => {
                let Some(value) = args.next() else {
                    return Ok(CliAction::Help(help()));
                };
                source_url = value.as_ref().to_owned();
            }
            other => return Err(format!("unsupported idx lint option: {other}")),
        }
    }

    let input = fs::read_to_string(manifest_path)
        .map_err(|error| format!("failed to read {manifest_path}: {error}"))?;
    let parsed = parse_index_manifest(&input, &source_url, page_url);

    match parsed {
        Ok(manifest) => Ok(CliAction::Print(idx_lint_pass_report(
            manifest_path,
            page_url,
            &manifest,
        ))),
        Err(error) => Err(idx_lint_failure_report(
            manifest_path,
            page_url,
            &source_url,
            &error,
        )),
    }
}

fn idx_lint_pass_report(manifest_path: &str, page_url: &str, manifest: &IndexManifest) -> String {
    format!(
        "index-idx-lint-v1\nmanifest: {manifest_path}\npage_url: {page_url}\nsource_url: {}\nstatus: pass\nscope: {}\ncontent.main_selector: {}\nregions: {}\nfields: {}\nforms: {}\ndates: {}\nchecks: same-origin=pass scope=pass safety=pass\nresult: pass",
        manifest.source_url,
        manifest.scope,
        manifest.content.main_selector.as_deref().unwrap_or("-"),
        manifest.regions.len(),
        manifest.fields.len(),
        manifest.forms.len(),
        manifest.dates.len(),
    )
}

fn idx_lint_failure_report(
    manifest_path: &str,
    page_url: &str,
    source_url: &str,
    error: &IndexManifestError,
) -> String {
    format!(
        "index-idx-lint-v1\nmanifest: {manifest_path}\npage_url: {page_url}\nsource_url: {source_url}\nstatus: fail\nerror: {error}\naction: {}\nresult: fail",
        idx_lint_fix(error),
    )
}

fn idx_lint_fix(error: &IndexManifestError) -> &'static str {
    match error {
        IndexManifestError::TooLarge { .. } => {
            "reduce manifest size below 32KiB and remove non-essential hints"
        }
        IndexManifestError::InvalidJson(_) => "fix JSON syntax and validate with a local formatter",
        IndexManifestError::UnsupportedVersion(_) => "set version to index.idx/v1",
        IndexManifestError::InvalidSourceUrl(_) => {
            "pass a valid same-origin source URL with --source-url"
        }
        IndexManifestError::InvalidPageUrl(_) => "provide a valid page URL for lint context",
        IndexManifestError::CrossOrigin { .. } => {
            "serve index.idx from the same origin as the target page"
        }
        IndexManifestError::InvalidScope(_) => "use an absolute scope path (for example /docs)",
        IndexManifestError::OutOfScope { .. } => "adjust scope so it includes the tested page path",
        IndexManifestError::TooManyHints { .. } => {
            "reduce hint count in this category to <= 64 entries"
        }
        IndexManifestError::InvalidHint { .. } => {
            "simplify selectors/text values and keep hints within protocol limits"
        }
    }
}

fn run_artifact_command<I, S>(mut args: I) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let Some(command) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    match command.as_ref() {
        "inspect" => {
            let Some(target) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let normalized = normalize_url_input(target.as_ref());
            let canonical = IndexUrl::parse(&normalized).map_err(|error| error.to_string())?;
            let store = ArtifactStore::new(artifact_store_root());
            let mut lines = vec![
                "index-artifact-inspect-v1".to_owned(),
                format!("target: {normalized}"),
                format!("store_root: {}", store.root().display()),
            ];
            for context in [
                ArtifactContext::LiveGet,
                ArtifactContext::LiveSubmit,
                ArtifactContext::Offline,
            ] {
                match store
                    .load(&canonical, context)
                    .map_err(|error| error.to_string())?
                {
                    Some(artifact) => {
                        let freshness = match artifact.freshness(unix_timestamp_secs()) {
                            ArtifactFreshness::Fresh => "fresh",
                            ArtifactFreshness::Stale => "stale",
                        };
                        lines.push(format!(
                            "context: {}\n  status: present\n  freshness: {freshness}\n  canonical_url: {}\n  final_url: {}\n  stored_at_unix_secs: {}\n  max_age_secs: {}",
                            context.as_str(),
                            artifact.canonical_url,
                            artifact.final_url,
                            artifact.stored_at_unix_secs,
                            artifact.max_age_secs
                        ));
                    }
                    None => {
                        lines.push(format!("context: {}\n  status: missing", context.as_str()));
                    }
                }
            }
            Ok(CliAction::Print(lines.join("\n")))
        }
        other => Err(format!("unsupported artifact command: {other}")),
    }
}

fn adapter_check_report(fixture: &str) -> Result<String, String> {
    let document = load_offline_document(fixture)?;
    let quality = document.metadata.quality.as_ref();
    let adapter = document
        .metadata
        .adapter_id
        .as_ref()
        .map_or("none", |id| id.as_str());
    let category = quality.map_or("unknown", |quality| quality.category.as_str());
    let score = quality.map_or(0, |quality| quality.score);
    let fallback = if adapter == "none" {
        quality.and_then(|quality| quality.reasons.first()).map_or(
            "no adapter matched; generic transformer used",
            String::as_str,
        )
    } else {
        "adapter emitted task view"
    };
    let counts = adapter_counts(&document);
    let markdown = extract_document(&document, ExtractFormat::Markdown);

    Ok(format!(
        "index-adapter-check-v1\nfixture: {fixture}\nadapter: {adapter}\nsupport_tier: {}\nquality: {category}\nquality_score: {score}\nfallback_reason: {fallback}\nnodes: {}\nlinks: {}\nforms: {}\ntables: {}\nregions: {}\nchecklist: docs/FIXTURE_INTAKE.md\n--- markdown ---\n{markdown}",
        adapter_support_tier(category),
        counts.nodes,
        counts.links,
        counts.forms,
        counts.tables,
        counts.regions,
    ))
}

fn adapter_support_tier(category: &str) -> u8 {
    match category {
        "adapter" => 3,
        "strong-generic" => 2,
        "partial-generic" | "fallback" => 1,
        "failed" => 0,
        _ => 0,
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct AdapterManifest {
    adapter: String,
    support_tier: u8,
    nodes: usize,
    links: usize,
    forms: usize,
    tables: usize,
    regions: usize,
    markdown_contains: Vec<String>,
}

fn adapter_manifest_scaffold(fixture: &str) -> Result<String, String> {
    let document = load_offline_document(fixture)?;
    let quality = document.metadata.quality.as_ref();
    let adapter = document
        .metadata
        .adapter_id
        .as_ref()
        .map_or("none", |id| id.as_str());
    let category = quality.map_or("unknown", |quality| quality.category.as_str());
    let counts = adapter_counts(&document);
    let markdown = extract_document(&document, ExtractFormat::Markdown);
    let title_line = markdown.lines().next().unwrap_or_default();

    Ok(format!(
        "index-adapter-manifest-v1\nfixture: {fixture}\nadapter: {adapter}\nsupport_tier: {}\nnodes: {}\nlinks: {}\nforms: {}\ntables: {}\nregions: {}\nmarkdown_contains: {}\n",
        adapter_support_tier(category),
        counts.nodes,
        counts.links,
        counts.forms,
        counts.tables,
        counts.regions,
        title_line
    ))
}

fn adapter_manifest_diff(fixture: &str, manifest_path: &str) -> Result<String, String> {
    let expected = parse_adapter_manifest(
        &fs::read_to_string(manifest_path)
            .map_err(|error| format!("failed to read {manifest_path}: {error}"))?,
    )?;
    let document = load_offline_document(fixture)?;
    let quality = document.metadata.quality.as_ref();
    let adapter = document
        .metadata
        .adapter_id
        .as_ref()
        .map_or("none", |id| id.as_str());
    let category = quality.map_or("unknown", |quality| quality.category.as_str());
    let counts = adapter_counts(&document);
    let markdown = extract_document(&document, ExtractFormat::Markdown);

    let mut mismatches = Vec::new();
    push_manifest_mismatch(&mut mismatches, "adapter", &expected.adapter, adapter);
    push_manifest_mismatch(
        &mut mismatches,
        "support_tier",
        &expected.support_tier.to_string(),
        &adapter_support_tier(category).to_string(),
    );
    push_manifest_mismatch(
        &mut mismatches,
        "nodes",
        &expected.nodes.to_string(),
        &counts.nodes.to_string(),
    );
    push_manifest_mismatch(
        &mut mismatches,
        "links",
        &expected.links.to_string(),
        &counts.links.to_string(),
    );
    push_manifest_mismatch(
        &mut mismatches,
        "forms",
        &expected.forms.to_string(),
        &counts.forms.to_string(),
    );
    push_manifest_mismatch(
        &mut mismatches,
        "tables",
        &expected.tables.to_string(),
        &counts.tables.to_string(),
    );
    push_manifest_mismatch(
        &mut mismatches,
        "regions",
        &expected.regions.to_string(),
        &counts.regions.to_string(),
    );
    for needle in &expected.markdown_contains {
        if !markdown.contains(needle) {
            mismatches.push(format!("markdown_contains missing: {needle}"));
        }
    }

    let status = if mismatches.is_empty() {
        "ok"
    } else {
        "changed"
    };
    let mut output = format!(
        "index-adapter-diff-v1\nfixture: {fixture}\nmanifest: {manifest_path}\nstatus: {status}\n"
    );
    for mismatch in mismatches {
        output.push_str("mismatch: ");
        output.push_str(&mismatch);
        output.push('\n');
    }
    Ok(output)
}

fn parse_adapter_manifest(input: &str) -> Result<AdapterManifest, String> {
    let mut lines = input.lines();
    if lines.next() != Some("index-adapter-manifest-v1") {
        return Err("adapter manifest missing header".to_owned());
    }
    let mut adapter = None;
    let mut support_tier = None;
    let mut counts = AdapterCounts::default();
    let mut markdown_contains = Vec::new();
    for line in lines {
        let Some((key, value)) = line.split_once(": ") else {
            continue;
        };
        match key {
            "adapter" => adapter = Some(value.to_owned()),
            "support_tier" => support_tier = Some(parse_manifest_usize(key, value)? as u8),
            "nodes" => counts.nodes = parse_manifest_usize(key, value)?,
            "links" => counts.links = parse_manifest_usize(key, value)?,
            "forms" => counts.forms = parse_manifest_usize(key, value)?,
            "tables" => counts.tables = parse_manifest_usize(key, value)?,
            "regions" => counts.regions = parse_manifest_usize(key, value)?,
            "markdown_contains" => markdown_contains.push(value.to_owned()),
            _ => {}
        }
    }
    Ok(AdapterManifest {
        adapter: adapter.ok_or_else(|| "adapter manifest missing adapter".to_owned())?,
        support_tier: support_tier
            .ok_or_else(|| "adapter manifest missing support_tier".to_owned())?,
        nodes: counts.nodes,
        links: counts.links,
        forms: counts.forms,
        tables: counts.tables,
        regions: counts.regions,
        markdown_contains,
    })
}

fn parse_manifest_usize(key: &str, value: &str) -> Result<usize, String> {
    value
        .parse::<usize>()
        .map_err(|error| format!("invalid {key} in adapter manifest: {error}"))
}

fn push_manifest_mismatch(mismatches: &mut Vec<String>, key: &str, expected: &str, actual: &str) {
    if expected != actual {
        mismatches.push(format!("{key} expected {expected} actual {actual}"));
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct AdapterCounts {
    nodes: usize,
    links: usize,
    forms: usize,
    tables: usize,
    regions: usize,
}

fn adapter_counts(document: &IndexDocument) -> AdapterCounts {
    let mut counts = AdapterCounts::default();
    for node in &document.nodes {
        count_adapter_node(node, &mut counts);
    }
    counts
}

fn count_adapter_node(node: &IndexNode, counts: &mut AdapterCounts) {
    counts.nodes += 1;
    match node {
        IndexNode::Link(_) => counts.links += 1,
        IndexNode::Form(_) => counts.forms += 1,
        IndexNode::Table { .. } => counts.tables += 1,
        IndexNode::Section { nodes, .. } => {
            counts.regions += 1;
            for child in nodes {
                count_adapter_node(child, counts);
            }
        }
        _ => {}
    }
}

fn run_shelf_command<I, S, F>(
    args: I,
    stdin: &mut dyn Read,
    fetcher: &F,
) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
    F: Fetcher,
{
    let paths = shelf_paths()?;
    run_shelf_command_with_paths(args, stdin, fetcher, &paths)
}

fn run_shelf_command_with_paths<I, S, F>(
    mut args: I,
    stdin: &mut dyn Read,
    fetcher: &F,
    paths: &ShelfPaths,
) -> Result<CliAction, String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
    F: Fetcher,
{
    let Some(command) = args.next() else {
        return Ok(CliAction::Help(help()));
    };
    match command.as_ref() {
        "save" => {
            let Some(input) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            run_shelf_save(input.as_ref(), stdin, fetcher, paths)
        }
        "list" => {
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let shelf = load_shelf(paths)?;
            Ok(CliAction::Print(format_shelf_list(&shelf)))
        }
        "show" => {
            let Some(id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            let shelf = load_shelf(paths)?;
            let record = shelf
                .get(id.as_ref())
                .ok_or_else(|| format!("shelf record not found: {}", id.as_ref()))?;
            Ok(CliAction::Print(format_shelf_record(record)))
        }
        "search" => {
            let (format, query) = parse_shelf_search_args(args)?;
            if query.trim().is_empty() {
                return Ok(CliAction::Help(help()));
            }
            let shelf = load_shelf(paths)?;
            let results = shelf.search(&query, |record| {
                fs::read_to_string(&record.markdown_path).ok()
            });
            Ok(CliAction::Print(format_shelf_search(
                &query, format, &results,
            )))
        }
        "tag" => {
            let Some(id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let Some(tag) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            if args.next().is_some() {
                return Err("too many arguments".to_owned());
            }
            update_shelf_record(id.as_ref(), |record| record.add_tag(tag.as_ref()), paths)?;
            Ok(CliAction::Print(format!(
                "shelf-tagged\t{}\t{}",
                id.as_ref(),
                tag.as_ref()
            )))
        }
        "note" => {
            let Some(id) = args.next() else {
                return Ok(CliAction::Help(help()));
            };
            let note = args
                .map(|part| part.as_ref().to_owned())
                .collect::<Vec<_>>()
                .join(" ");
            if note.trim().is_empty() {
                return Ok(CliAction::Help(help()));
            }
            update_shelf_record(id.as_ref(), |record| record.set_note(note.clone()), paths)?;
            Ok(CliAction::Print(format!("shelf-noted\t{}", id.as_ref())))
        }
        other => Err(format!("unsupported shelf command: {other}")),
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ShelfSearchFormat {
    Links,
    Markdown,
    Json,
}

impl ShelfSearchFormat {
    fn parse(input: &str) -> Option<Self> {
        match input {
            "links" => Some(Self::Links),
            "markdown" => Some(Self::Markdown),
            "json" => Some(Self::Json),
            _ => None,
        }
    }
}

fn parse_shelf_search_args<I, S>(args: I) -> Result<(ShelfSearchFormat, String), String>
where
    I: Iterator<Item = S>,
    S: AsRef<str>,
{
    let mut format = ShelfSearchFormat::Links;
    let mut query = Vec::new();
    let mut args = args.peekable();
    while let Some(arg) = args.next() {
        if arg.as_ref() == "--format" {
            let Some(next_format) = args.next() else {
                return Err("missing shelf search format".to_owned());
            };
            format = ShelfSearchFormat::parse(next_format.as_ref()).ok_or_else(|| {
                format!("unsupported shelf search format: {}", next_format.as_ref())
            })?;
        } else {
            query.push(arg.as_ref().to_owned());
            query.extend(args.map(|part| part.as_ref().to_owned()));
            break;
        }
    }
    Ok((format, query.join(" ")))
}

fn run_shelf_save<F: Fetcher>(
    input: &str,
    stdin: &mut dyn Read,
    fetcher: &F,
    paths: &ShelfPaths,
) -> Result<CliAction, String> {
    let document = load_document(input, stdin, fetcher)?;
    fs::create_dir_all(&paths.exports_dir).map_err(|error| error.to_string())?;
    let source_url = document.metadata.canonical_url.clone();
    let quality = document
        .metadata
        .quality
        .as_ref()
        .map(|quality| quality.category.as_str().to_owned());
    let citations = citation_urls(&document);
    let mut record = ShelfRecord::new(
        document.title.clone(),
        source_url,
        quality,
        unix_timestamp(),
        citations,
        String::new(),
        String::new(),
    );
    record.markdown_path = paths
        .exports_dir
        .join(format!("{}.md", record.id))
        .display()
        .to_string();
    record.json_path = paths
        .exports_dir
        .join(format!("{}.json", record.id))
        .display()
        .to_string();
    fs::write(
        &record.markdown_path,
        extract_document(&document, ExtractFormat::Markdown),
    )
    .map_err(|error| error.to_string())?;
    fs::write(
        &record.json_path,
        extract_document(&document, ExtractFormat::Json),
    )
    .map_err(|error| error.to_string())?;
    let mut shelf = load_shelf(paths)?;
    shelf.upsert(record.clone());
    save_shelf(&shelf, paths)?;
    Ok(CliAction::Print(format!(
        "shelf-saved\t{}\t{}",
        record.id, record.title
    )))
}

fn update_shelf_record(
    id: &str,
    update: impl FnOnce(&mut ShelfRecord),
    paths: &ShelfPaths,
) -> Result<(), String> {
    let mut shelf = load_shelf(paths)?;
    let record = shelf
        .get_mut(id)
        .ok_or_else(|| format!("shelf record not found: {id}"))?;
    update(record);
    save_shelf(&shelf, paths)
}

fn load_shelf(paths: &ShelfPaths) -> Result<KnowledgeShelf, String> {
    KnowledgeShelf::load_from_path(&paths.index_path).map_err(|error| error.to_string())
}

fn save_shelf(shelf: &KnowledgeShelf, paths: &ShelfPaths) -> Result<(), String> {
    fs::create_dir_all(&paths.shelf_dir).map_err(|error| error.to_string())?;
    shelf
        .save_to_path(&paths.index_path)
        .map_err(|error| error.to_string())
}

#[derive(Debug, Clone)]
struct ShelfPaths {
    shelf_dir: std::path::PathBuf,
    exports_dir: std::path::PathBuf,
    index_path: std::path::PathBuf,
}

fn shelf_paths() -> Result<ShelfPaths, String> {
    let shelf_dir = std::path::PathBuf::from(runtime_paths().state_dir).join("shelf");
    let exports_dir = shelf_dir.join("exports");
    let index_path = shelf_dir.join("index-shelf.txt");
    fs::create_dir_all(&shelf_dir).map_err(|error| error.to_string())?;
    Ok(ShelfPaths {
        shelf_dir,
        exports_dir,
        index_path,
    })
}

fn format_shelf_list(shelf: &KnowledgeShelf) -> String {
    let mut lines = vec!["index-shelf-list-v1".to_owned()];
    for record in shelf.iter() {
        lines.push(format!(
            "{}\t{}\t{}\t{}",
            record.id,
            record.title,
            record.quality.as_deref().unwrap_or("unknown"),
            record.source_url.as_deref().unwrap_or("")
        ));
    }
    lines.join("\n")
}

fn format_shelf_record(record: &ShelfRecord) -> String {
    format!(
        "index-shelf-record-v1\nid: {}\ntitle: {}\nsource_url: {}\nquality: {}\nsaved_at: {}\ntags: {}\nnote: {}\ncitations: {}\nmarkdown: {}\njson: {}",
        record.id,
        record.title,
        record.source_url.as_deref().unwrap_or(""),
        record.quality.as_deref().unwrap_or("unknown"),
        record.saved_at,
        record.tags.join(","),
        record.note.as_deref().unwrap_or(""),
        record.citations.join(","),
        record.markdown_path,
        record.json_path
    )
}

fn format_shelf_search(
    query: &str,
    format: ShelfSearchFormat,
    results: &[ShelfSearchResult],
) -> String {
    match format {
        ShelfSearchFormat::Links => format_shelf_search_links(query, results),
        ShelfSearchFormat::Markdown => format_shelf_search_markdown(query, results),
        ShelfSearchFormat::Json => format_shelf_search_json(query, results),
    }
}

fn format_shelf_search_links(query: &str, results: &[ShelfSearchResult]) -> String {
    let mut lines = vec![
        "index-shelf-search-v1".to_owned(),
        format!("query: {query}"),
        "format: links".to_owned(),
    ];
    for (index, result) in results.iter().enumerate() {
        lines.push(format!(
            "{}\t{}\t{}\t{}\t{}\t{}",
            index + 1,
            result.score,
            result.id,
            result.title,
            result.source_url.as_deref().unwrap_or(""),
            result.matched_fields.join(",")
        ));
    }
    lines.join("\n")
}

fn format_shelf_search_markdown(query: &str, results: &[ShelfSearchResult]) -> String {
    let mut output = format!("# Shelf search: {query}\n\n");
    for result in results {
        output.push_str("- ");
        if let Some(source_url) = &result.source_url {
            output.push_str(&format!("[{}]({source_url})", result.title));
        } else {
            output.push_str(&result.title);
        }
        output.push_str(&format!(
            " `{}` score {} fields {}\n",
            result.id,
            result.score,
            result.matched_fields.join(",")
        ));
    }
    output
}

fn format_shelf_search_json(query: &str, results: &[ShelfSearchResult]) -> String {
    let mut output = String::from("{\"query\":");
    push_json_string(&mut output, query);
    output.push_str(",\"results\":[");
    for (index, result) in results.iter().enumerate() {
        if index > 0 {
            output.push(',');
        }
        output.push_str("{\"id\":");
        push_json_string(&mut output, &result.id);
        output.push_str(",\"title\":");
        push_json_string(&mut output, &result.title);
        output.push_str(",\"source_url\":");
        if let Some(source_url) = &result.source_url {
            push_json_string(&mut output, source_url);
        } else {
            output.push_str("null");
        }
        output.push_str(",\"score\":");
        output.push_str(&result.score.to_string());
        output.push_str(",\"matched_fields\":[");
        for (field_index, field) in result.matched_fields.iter().enumerate() {
            if field_index > 0 {
                output.push(',');
            }
            push_json_string(&mut output, field);
        }
        output.push_str("]}");
    }
    output.push_str("]}");
    output
}

fn push_json_string(output: &mut String, value: &str) {
    output.push('"');
    for ch in value.chars() {
        match ch {
            '"' => output.push_str("\\\""),
            '\\' => output.push_str("\\\\"),
            '\n' => output.push_str("\\n"),
            '\r' => output.push_str("\\r"),
            '\t' => output.push_str("\\t"),
            ch if ch.is_control() => {
                output.push_str("\\u");
                output.push_str(&format!("{:04x}", ch as u32));
            }
            ch => output.push(ch),
        }
    }
    output.push('"');
}

fn citation_urls(document: &IndexDocument) -> Vec<String> {
    extract_citations_tsv(document)
        .lines()
        .skip(1)
        .filter_map(|line| line.split('\t').nth(2).map(ToOwned::to_owned))
        .collect()
}

fn unix_timestamp() -> String {
    unix_timestamp_secs().to_string()
}

fn unix_timestamp_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.as_secs())
}

fn load_offline_document(input: &str) -> Result<IndexDocument, String> {
    let contents = fs::read_to_string(input).map_err(|error| error.to_string())?;
    if let Ok(artifact) = CaptureArtifact::from_text(&contents) {
        let source_url =
            IndexUrl::parse(&artifact.source_url).map_err(|error| error.to_string())?;
        return Ok(transform_html(html_with_base(
            &source_url,
            &artifact.redacted_html,
        )));
    }
    Ok(transform_html(contents))
}

fn run_benchmark_command<F: Fetcher>(
    input: &str,
    stdin: &mut dyn Read,
    fetcher: &F,
) -> Result<CliAction, String> {
    let mut cache = TransformedDocumentCache::new();
    let source = benchmark_source(input, stdin, fetcher)?;
    let first_start = Instant::now();
    let first = transform_html_cached(
        &mut cache,
        source.source_url.as_deref(),
        source.html.clone(),
    );
    let first_elapsed = first_start.elapsed().as_micros();
    let second_start = Instant::now();
    let second = transform_html_cached(
        &mut cache,
        source.source_url.as_deref(),
        source.html.clone(),
    );
    let second_elapsed = second_start.elapsed().as_micros();
    let report = BenchmarkReport {
        source_label: source.label,
        bytes: source.html.len(),
        title: first.title,
        nodes: document_node_count(&second),
        links: document_link_count(&second),
        cache_entries: cache.len(),
        first_transform_micros: first_elapsed,
        cached_transform_micros: second_elapsed,
    };
    Ok(CliAction::Print(report.display()))
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct BenchmarkSource {
    label: String,
    source_url: Option<String>,
    html: String,
}

fn benchmark_source<F: Fetcher>(
    input: &str,
    stdin: &mut dyn Read,
    fetcher: &F,
) -> Result<BenchmarkSource, String> {
    if should_fetch_input(input) {
        let normalized = normalize_url_input(input);
        let url = IndexUrl::parse(&normalized).map_err(|error| error.to_string())?;
        let response = fetcher
            .fetch(&Request { url })
            .map_err(|error| error.to_string())?;
        return Ok(BenchmarkSource {
            label: normalized,
            source_url: Some(response.final_url.to_string()),
            html: html_with_base(&response.final_url, &response.body),
        });
    }

    Ok(BenchmarkSource {
        label: input.to_owned(),
        source_url: None,
        html: read_input(input, stdin)?,
    })
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct BenchmarkReport {
    source_label: String,
    bytes: usize,
    title: String,
    nodes: usize,
    links: usize,
    cache_entries: usize,
    first_transform_micros: u128,
    cached_transform_micros: u128,
}

impl BenchmarkReport {
    fn display(&self) -> String {
        format!(
            "index-benchmark-v1\nsource: {}\nbytes: {}\ntitle: {}\nnodes: {}\nlinks: {}\ncache_entries: {}\nfirst_transform_micros: {}\ncached_transform_micros: {}",
            self.source_label,
            self.bytes,
            self.title,
            self.nodes,
            self.links,
            self.cache_entries,
            self.first_transform_micros,
            self.cached_transform_micros
        )
    }
}

fn document_node_count(document: &IndexDocument) -> usize {
    document.nodes.iter().map(node_count).sum()
}

fn node_count(node: &IndexNode) -> usize {
    match node {
        IndexNode::Section { nodes, .. } => 1 + nodes.iter().map(node_count).sum::<usize>(),
        _ => 1,
    }
}

fn document_link_count(document: &IndexDocument) -> usize {
    document.nodes.iter().map(link_count).sum()
}

fn link_count(node: &IndexNode) -> usize {
    match node {
        IndexNode::Link(_) => 1,
        IndexNode::Section { nodes, .. } => nodes.iter().map(link_count).sum(),
        _ => 0,
    }
}

fn html_with_base(base_url: &IndexUrl, html: &str) -> String {
    if html.to_lowercase().contains("<base ") {
        return html.to_owned();
    }

    let base = format!(
        r#"<base href="{}">"#,
        base_url.as_str().replace('"', "&quot;")
    );
    let lower = html.to_lowercase();
    if let Some(head_start) = lower.find("<head") {
        if let Some(head_end) = html[head_start..].find('>') {
            let insert_at = head_start + head_end + 1;
            let mut output = String::with_capacity(html.len() + base.len());
            output.push_str(&html[..insert_at]);
            output.push_str(&base);
            output.push_str(&html[insert_at..]);
            return output;
        }
    }

    format!("<head>{base}</head>\n{html}")
}

fn is_network_url(input: &str) -> bool {
    input.starts_with("http://") || input.starts_with("https://")
}

fn should_fetch_input(input: &str) -> bool {
    is_network_url(input) || is_schemeless_url_input(input)
}

fn normalize_url_input(input: &str) -> String {
    let trimmed = input.trim();
    if has_url_scheme(trimmed) {
        trimmed.to_owned()
    } else {
        format!("https://{trimmed}")
    }
}

fn is_schemeless_url_input(input: &str) -> bool {
    let trimmed = input.trim();
    if trimmed.is_empty()
        || trimmed == "-"
        || has_url_scheme(trimmed)
        || trimmed.starts_with('/')
        || trimmed.starts_with("./")
        || trimmed.starts_with("../")
        || fs::metadata(trimmed).is_ok()
    {
        return false;
    }

    let authority = trimmed
        .split(['/', '?', '#'])
        .next()
        .unwrap_or_default()
        .trim_end_matches('.');
    authority.eq_ignore_ascii_case("localhost") || authority.contains('.')
}

fn has_url_scheme(input: &str) -> bool {
    let Some(index) = input.find(':') else {
        return false;
    };
    let scheme = &input[..index];
    !scheme.is_empty()
        && scheme
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '+' | '-' | '.'))
        && scheme
            .chars()
            .next()
            .is_some_and(|ch| ch.is_ascii_alphabetic())
}

fn read_input_with_limits(
    input: &str,
    stdin: &mut dyn Read,
    limits: ContentLimits,
) -> Result<String, String> {
    if input == "-" {
        let mut buffer = String::new();
        stdin
            .read_to_string(&mut buffer)
            .map_err(|error| format!("failed to read stdin: {error}"))?;
        check_content_size(&buffer, limits).map_err(|error| error.to_string())?;
        return Ok(buffer);
    }

    if is_network_url(input) {
        return Err(
            "network URLs must be opened through the browser entry point, not as local files"
                .to_owned(),
        );
    }

    let html =
        fs::read_to_string(input).map_err(|error| format!("failed to read {input}: {error}"))?;
    check_content_size(&html, limits).map_err(|error| error.to_string())?;
    Ok(html)
}

fn help() -> String {
    "Index terminal browser prototype\n\nUsage:\n  index\n  index <url-or-local-html-file>\n  index - < file.html\n  index quickstart\n  index --profile reader|docs|links|research|compact|verbose [url-or-local-html-file]\n  index --plain <url-or-local-html-file>\n  index --plain - < file.html\n  index --extract markdown|links|json <url-or-local-html-file>\n  index --extract markdown|links|json - < file.html\n  index --save markdown|json <url-or-local-html-file> <output-file>\n  index --citations <url-or-local-html-file>\n  index --section <heading> <url-or-local-html-file>\n  index --batch-extract markdown|json <local-html-or-capture-artifact>...\n  index --ai-offline explain|summarize|extract <url-or-local-html-file>\n  index --ai-offline explain|summarize|extract - < file.html\n  index --benchmark <url-or-local-html-file>\n  index --benchmark - < file.html\n  index doctor\n  index compatibility-slo [--top100 <matrix.tsv>] [--forum <matrix.tsv>] [--min-readability <0..1>] [--min-actionability <0..1>] [--min-failure-quality <0..1>]\n  index compatibility-slo-v2 [--top100 <matrix.tsv>] [--forum <matrix.tsv>] [--baseline-top100 <matrix.tsv>] [--baseline-forum <matrix.tsv>] [--min-readability <0..1>] [--min-actionability <0..1>] [--min-failure-quality <0..1>]\n  index compatibility-backlog [--top100 <matrix.tsv>] [--forum <matrix.tsv>] [--top <n>]\n  index compatibility-live-variance [--targets <targets.tsv>] [--runs <runs.tsv>] [--window <n>]\n  index compatibility-recovery-plan <url>\n  index compatibility-recovery-gate [--top100 <matrix.tsv>] [--forum <matrix.tsv>] [--live-targets <targets.tsv>] [--live-runs <runs.tsv>] [--live-window <n>] [--max-live-flake-rate <0..1>] [--max-live-timeout-rate <0..1>] [--min-readability <0..1>] [--min-actionability <0..1>] [--min-failure-quality <0..1>]\n  index compatibility-pack lint <pack-file> <page-url>\n  index compatibility-pack inspect <page-url>\n  index compatibility-pack install <pack-file> [--user|--trusted]\n  index compatibility-pack update <pack-id> <pack-file> [--user|--trusted]\n  index compatibility-pack list\n  index compatibility-pack remove <pack-id>\n  index compatibility-pack rollback <pack-id>\n  index compatibility-pack sign <pack-file> <key-id> <secret>\n  index compatibility-pack verify <pack-file> <key-id> <secret> <signature>\n  index auth-assist inspect <url>\n  index auth-assist import <cookie-bundle-file>\n  index auth-assist export <output-file>\n  index auth-assist diagnose-submit <url> <status-code> <message>\n  index challenge-diagnose <url> <message-or-html-file>\n  index idx lint <manifest-file> <page-url> [--source-url <manifest-url>]\n  index adapter check <local-html-or-capture-artifact>\n  index adapter scaffold <local-html-or-capture-artifact>\n  index adapter diff <local-html-or-capture-artifact> <manifest>\n  index artifact inspect <url>\n  index shelf save <url-or-local-html-file>\n  index shelf list\n  index shelf show <id>\n  index shelf search [--format links|markdown|json] <query>\n  index shelf tag <id> <tag>\n  index shelf note <id> <text>\n  index capture --redact <url> <local-html-file>\n  index capture --redact <url> - < file.html\n  index capture --preview --redact <url> <local-html-file>\n  index capture --validate <artifact-file>\n  index capture --catalog-entry <fixture-path>\n  index --paths\n  index --version\n\nURL targets without a scheme default to HTTPS.\n\nKeys:\n  j/k scroll, gg/G top/bottom, / search, f link hints, e edit form, tab next form field or open-history completion, enter submit form, :open <id-or-url>, :logs, :submit <form> field=value, :extract markdown|links|json, :pipe <cmd>, :ai explain|summarize|extract, :profile <name|auto>, :quit".to_owned()
}

fn version() -> String {
    format!("index {VERSION}")
}

fn quickstart() -> String {
    "index-quickstart-v1\nstep_1_open_url: index example.org\nstep_2_open_in_tui: :open <id-or-url>\nstep_3_find_links: press f\nstep_4_use_forms: press e, tab through fields, enter to submit\nstep_5_extract: :extract markdown|links|json\nstep_6_debug_network: index doctor and :logs\ntip: URL targets without a scheme default to HTTPS.".to_owned()
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct RuntimePaths {
    config_dir: String,
    cache_dir: String,
    state_dir: String,
}

impl RuntimePaths {
    fn display(&self) -> String {
        format!(
            "config_dir={}\ncache_dir={}\nstate_dir={}",
            self.config_dir, self.cache_dir, self.state_dir
        )
    }
}

fn doctor_report(paths: &RuntimePaths) -> String {
    let config = redact_home_path(&paths.config_dir);
    let cache = redact_home_path(&paths.cache_dir);
    let state = redact_home_path(&paths.state_dir);
    let config_check = path_health("config", &paths.config_dir);
    let cache_check = path_health("cache", &paths.cache_dir);
    let state_check = path_health("state", &paths.state_dir);

    format!(
        "index-doctor-v1\nversion={}\ntelemetry=local-only\nnetwork_probe=not-run\nnetwork_probe_policy=opt-in only; run normal URL commands when you choose to test live network access\nconfig_dir={config}\ncache_dir={cache}\nstate_dir={state}\nchecks:\n- {config_check}\n- {cache_check}\n- {state_check}\n- package: run `index --version` and compare with release notes\nsupport_report=review before sharing; no page content, cookies, or network probes are included",
        version()
    )
}

fn path_health(label: &str, path: &str) -> String {
    let path = std::path::Path::new(path);
    if path.is_dir() {
        format!("{label}: ok")
    } else {
        format!(
            "{label}: missing; remediation: create with `mkdir -p {}`",
            redact_home_path(&path.display().to_string())
        )
    }
}

fn redact_home_path(path: &str) -> String {
    let Ok(home) = env::var("HOME") else {
        return path.to_owned();
    };
    if home.trim().is_empty() {
        return path.to_owned();
    }
    if path == home {
        return "$HOME".to_owned();
    }
    if let Some(rest) = path.strip_prefix(&format!("{home}/")) {
        return format!("$HOME/{rest}");
    }
    path.to_owned()
}

fn runtime_paths() -> RuntimePaths {
    RuntimePaths {
        config_dir: format!("{}/index", env_dir("XDG_CONFIG_HOME", ".config")),
        cache_dir: format!("{}/index", env_dir("XDG_CACHE_HOME", ".cache")),
        state_dir: format!("{}/index", env_dir("XDG_STATE_HOME", ".local/state")),
    }
}

fn artifact_store_root() -> PathBuf {
    PathBuf::from(runtime_paths().cache_dir).join("artifacts")
}

fn artifact_runtime_enabled() -> bool {
    !cfg!(test)
}

fn env_dir(variable: &str, fallback_suffix: &str) -> String {
    if let Ok(value) = env::var(variable) {
        if !value.trim().is_empty() {
            return value;
        }
    }

    if let Ok(home) = env::var("HOME") {
        if !home.trim().is_empty() {
            return format!("{home}/{fallback_suffix}");
        }
    }

    format!("./{fallback_suffix}")
}

#[cfg(test)]
mod tests {
    use std::cell::RefCell;
    use std::io::Cursor;
    use std::time::{Duration, SystemTime, UNIX_EPOCH};

    use super::{
        AdapterManifest, CliAction, CompatibilityBacklogRow, CompatibilityCorpus,
        CompatibilityMilestone, CompatibilitySloFamilyDelta, CompatibilitySloRow,
        CompatibilitySloThresholds, CoverageExpectedPath, CoverageStatus, FamilyCompatibilityScore,
        IMAGE_PREVIEW_MAX_IMAGES_PER_DOCUMENT, LiveOutcome, LiveVarianceOriginStats,
        LiveVarianceReport, RecoveryProfile, RuntimePaths, RuntimeStage, ShelfPaths,
        ShelfSearchFormat, StageFlow, adapter_support_tier, auth_assist_diagnose_submit,
        candidate_manifest_urls, compatibility_slo_planning_inputs,
        compatibility_slo_v2_report_from_paths, default_start_page, doctor_report,
        document_from_index_artifact_uncached, document_link_count, document_node_count,
        enforce_stage_budget, evaluate_compatibility_backlog, evaluate_compatibility_slo,
        evaluate_compatibility_slo_v2, fetch_document_cached_with_log,
        fetch_document_cached_with_log_with_counter_with_progress,
        fetch_document_with_artifact_runtime, format_shelf_list, format_shelf_record,
        format_shelf_search, fs, html_with_base, hydrate_document_images_with_loader,
        is_valid_stage_transition, load_offline_document, map_backlog_row_to_milestone,
        normalize_url_input, parse_adapter_manifest, parse_forum_backlog_rows,
        parse_live_variance_rows, parse_live_variance_targets, parse_shelf_search_args,
        parse_slo_floor, parse_top100_backlog_rows, path_health, read_input,
        read_input_with_limits, recovery_fallback_order, redact_home_path,
        refresh_live_get_artifact, render_document, run_batch_extract_command, run_save_command,
        run_shelf_command_with_paths, run_with_args, run_with_args_and_fetcher, submit_form_cached,
        submit_form_cached_with_log, submit_form_cached_with_log_with_counter_with_progress,
        submit_form_with_artifact_runtime, version,
    };
    use index_core::{
        Form, IndexDocument, IndexNode, IndexUrl, Input, KnowledgeShelf, Link, ReaderProfile,
        SectionRole, ShelfRecord, ShelfSearchResult,
    };
    use index_http::{FetchError, MemoryFetcher, Response, SecureFetcher};
    use index_renderer::RenderOptions;
    use index_security::ContentLimits;
    use index_transformer::TransformedDocumentCache;

    fn unique_temp_file(name: &str) -> std::path::PathBuf {
        let nanos = match SystemTime::now().duration_since(UNIX_EPOCH) {
            Ok(duration) => duration.as_nanos(),
            Err(_) => 0,
        };

        std::env::temp_dir().join(format!("index-cli-{name}-{nanos}.html"))
    }

    fn workspace_path(path: &str) -> String {
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../..")
            .join(path)
            .display()
            .to_string()
    }

    #[test]
    fn stage_flow_accepts_valid_runtime_sequence() {
        let mut flow = StageFlow::default();
        assert!(flow.advance(RuntimeStage::Queued).is_ok());
        assert!(flow.advance(RuntimeStage::Fetching).is_ok());
        assert!(flow.advance(RuntimeStage::Snapshotting).is_ok());
        assert!(flow.advance(RuntimeStage::Parsing).is_ok());
        assert!(flow.advance(RuntimeStage::Transforming).is_ok());
        assert!(flow.advance(RuntimeStage::Scoring).is_ok());
        assert!(flow.advance(RuntimeStage::Storing).is_ok());
        assert!(flow.advance(RuntimeStage::Done).is_ok());
    }

    #[test]
    fn stage_flow_rejects_invalid_runtime_sequence() {
        assert!(!is_valid_stage_transition(
            Some(RuntimeStage::Queued),
            RuntimeStage::Done
        ));
        let mut flow = StageFlow::default();
        assert!(flow.advance(RuntimeStage::Queued).is_ok());
        assert!(flow.advance(RuntimeStage::Done).is_err());
    }

    #[test]
    fn stage_budget_enforcement_flags_timeout_excess() {
        let target = "https://example.org/slow";
        let ok = enforce_stage_budget(RuntimeStage::Fetching, Duration::from_secs(1), target);
        assert!(ok.is_ok());
        let too_slow =
            enforce_stage_budget(RuntimeStage::Fetching, Duration::from_secs(121), target);
        assert!(matches!(too_slow, Err(message) if message.contains("timed out")));
    }

    fn slo_row(
        corpus: CompatibilityCorpus,
        domain: &str,
        min_tier: u8,
        current_tier: u8,
        expected_path: CoverageExpectedPath,
        status: CoverageStatus,
        known_limit: &str,
    ) -> CompatibilitySloRow {
        CompatibilitySloRow {
            corpus,
            domain: domain.to_owned(),
            family: if corpus == CompatibilityCorpus::Top100 {
                "search-portal".to_owned()
            } else {
                "legacy-forum".to_owned()
            },
            intent: (corpus == CompatibilityCorpus::Top100).then(|| "search-results".to_owned()),
            min_tier,
            current_tier,
            expected_path,
            status,
            known_limit: known_limit.to_owned(),
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn slo_row_with_family(
        corpus: CompatibilityCorpus,
        domain: &str,
        family: &str,
        intent: Option<&str>,
        min_tier: u8,
        current_tier: u8,
        expected_path: CoverageExpectedPath,
        status: CoverageStatus,
        known_limit: &str,
    ) -> CompatibilitySloRow {
        CompatibilitySloRow {
            corpus,
            domain: domain.to_owned(),
            family: family.to_owned(),
            intent: intent.map(ToOwned::to_owned),
            min_tier,
            current_tier,
            expected_path,
            status,
            known_limit: known_limit.to_owned(),
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn backlog_row(
        corpus: CompatibilityCorpus,
        domain: &str,
        family: &str,
        intent: Option<&str>,
        min_tier: u8,
        current_tier: u8,
        expected_path: CoverageExpectedPath,
        status: CoverageStatus,
        known_limit: &str,
    ) -> CompatibilityBacklogRow {
        CompatibilityBacklogRow {
            corpus,
            domain: domain.to_owned(),
            family: family.to_owned(),
            intent: intent.map(ToOwned::to_owned),
            min_tier,
            current_tier,
            expected_path,
            status,
            known_limit: known_limit.to_owned(),
        }
    }

    #[test]
    fn compatibility_slo_threshold_validation_enforces_floors() {
        let top100_rows = vec![
            slo_row(
                CompatibilityCorpus::Top100,
                "alpha.example",
                1,
                1,
                CoverageExpectedPath::Generic,
                CoverageStatus::Covered,
                "none",
            ),
            slo_row(
                CompatibilityCorpus::Top100,
                "beta.example",
                1,
                0,
                CoverageExpectedPath::Generic,
                CoverageStatus::Planned,
                "none",
            ),
            slo_row(
                CompatibilityCorpus::Top100,
                "blocked.example",
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "auth-wall",
            ),
        ];
        let forum_rows = vec![
            slo_row(
                CompatibilityCorpus::Forum,
                "forum.example",
                1,
                2,
                CoverageExpectedPath::Adapter,
                CoverageStatus::Covered,
                "none",
            ),
            slo_row(
                CompatibilityCorpus::Forum,
                "forum-blocked.example",
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Planned,
                "js-required",
            ),
        ];
        let strict = CompatibilitySloThresholds {
            readability_floor: 0.9,
            actionability_floor: 0.7,
            failure_quality_floor: 1.0,
        };

        let report = evaluate_compatibility_slo(
            "top100.tsv",
            "forum.tsv",
            &top100_rows,
            &forum_rows,
            strict,
        );
        assert!(!report.passed());
        assert!(
            report
                .violations
                .iter()
                .any(|line| line.contains("readability score"))
        );
        assert!(
            report
                .violations
                .iter()
                .any(|line| line.contains("actionability score"))
        );
    }

    #[test]
    fn compatibility_slo_scores_are_deterministic_for_row_order() {
        let rows_a = vec![
            slo_row(
                CompatibilityCorpus::Top100,
                "one.example",
                1,
                2,
                CoverageExpectedPath::Adapter,
                CoverageStatus::Covered,
                "none",
            ),
            slo_row(
                CompatibilityCorpus::Top100,
                "two.example",
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "auth-wall",
            ),
        ];
        let rows_b = vec![rows_a[1].clone(), rows_a[0].clone()];
        let forum_rows = vec![slo_row(
            CompatibilityCorpus::Forum,
            "forum.example",
            1,
            2,
            CoverageExpectedPath::Adapter,
            CoverageStatus::Covered,
            "none",
        )];
        let thresholds = CompatibilitySloThresholds::default();
        let report_a = evaluate_compatibility_slo(
            "a-top100.tsv",
            "a-forum.tsv",
            &rows_a,
            &forum_rows,
            thresholds,
        );
        let report_b = evaluate_compatibility_slo(
            "b-top100.tsv",
            "b-forum.tsv",
            &rows_b,
            &forum_rows,
            thresholds,
        );
        assert_eq!(report_a.readability_score, report_b.readability_score);
        assert_eq!(report_a.actionability_score, report_b.actionability_score);
        assert_eq!(
            report_a.failure_quality_score,
            report_b.failure_quality_score
        );
        assert_eq!(report_a.violations, report_b.violations);
    }

    #[test]
    fn compatibility_slo_failure_guardrail_rejects_bad_failure_rows() {
        let top100_rows = vec![slo_row(
            CompatibilityCorpus::Top100,
            "bad-failure.example",
            0,
            0,
            CoverageExpectedPath::Failure,
            CoverageStatus::Covered,
            "none",
        )];
        let forum_rows = vec![slo_row(
            CompatibilityCorpus::Forum,
            "forum-ok.example",
            1,
            2,
            CoverageExpectedPath::Adapter,
            CoverageStatus::Covered,
            "none",
        )];
        let report = evaluate_compatibility_slo(
            "top100.tsv",
            "forum.tsv",
            &top100_rows,
            &forum_rows,
            CompatibilitySloThresholds {
                readability_floor: 0.0,
                actionability_floor: 0.0,
                failure_quality_floor: 1.0,
            },
        );
        assert!(!report.passed());
        assert!(
            report
                .violations
                .iter()
                .any(|line| line.contains("failure guardrail violations"))
        );
        assert!(
            report
                .violations
                .iter()
                .any(|line| line.contains("top100:bad-failure.example"))
        );
    }

    #[test]
    fn compatibility_slo_v2_family_thresholds_enforce_per_family_floors() {
        let top100_rows = vec![
            slo_row_with_family(
                CompatibilityCorpus::Top100,
                "search-low.example",
                "search-portal",
                Some("search-results"),
                1,
                0,
                CoverageExpectedPath::Adapter,
                CoverageStatus::Planned,
                "none",
            ),
            slo_row_with_family(
                CompatibilityCorpus::Top100,
                "search-failure.example",
                "search-portal",
                Some("search-results"),
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "auth-wall",
            ),
        ];
        let forum_rows = Vec::new();

        let report = evaluate_compatibility_slo_v2(
            "top100.tsv",
            "forum.tsv",
            &top100_rows,
            &forum_rows,
            CompatibilitySloThresholds {
                readability_floor: 0.0,
                actionability_floor: 0.0,
                failure_quality_floor: 0.0,
            },
            None,
        );

        assert!(!report.passed());
        assert!(
            report
                .violations
                .iter()
                .any(|line| line.contains("family search-portal"))
        );
        assert!(
            report
                .planning_inputs
                .iter()
                .any(|line| line.contains("M74: readability gap"))
        );
    }

    #[test]
    fn compatibility_slo_v2_reports_evidence_gaps_for_known_limit_only_families() {
        let top100_rows = vec![
            slo_row_with_family(
                CompatibilityCorpus::Top100,
                "blocked-search.example",
                "search-portal",
                Some("search-results"),
                1,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "auth-wall",
            ),
            slo_row_with_family(
                CompatibilityCorpus::Top100,
                "blocked-reddit.example",
                "reddit",
                Some("thread"),
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "script-gate",
            ),
        ];
        let forum_rows = Vec::new();

        let report = evaluate_compatibility_slo_v2(
            "top100.tsv",
            "forum.tsv",
            &top100_rows,
            &forum_rows,
            CompatibilitySloThresholds {
                readability_floor: 0.0,
                actionability_floor: 0.0,
                failure_quality_floor: 0.0,
            },
            None,
        );

        assert_eq!(report.evidence_warnings.len(), 1);
        assert!(
            report
                .evidence_warnings
                .iter()
                .any(|line| line.contains("family search-portal"))
        );
        assert!(
            report
                .planning_inputs
                .iter()
                .any(|line| line == "M73: evidence-gap backlog for family search-portal")
        );
        assert!(report.to_text().contains("evidence_warnings:"));
    }

    #[test]
    fn compatibility_slo_v2_canonicalizes_reddit_family_aliases() {
        let top100_rows = vec![slo_row_with_family(
            CompatibilityCorpus::Top100,
            "reddit-covered.example",
            "social-community",
            Some("feed-or-thread"),
            1,
            2,
            CoverageExpectedPath::Adapter,
            CoverageStatus::Covered,
            "none",
        )];
        let forum_rows = vec![slo_row_with_family(
            CompatibilityCorpus::Forum,
            "reddit-blocked.example",
            "reddit",
            Some("thread"),
            1,
            1,
            CoverageExpectedPath::Failure,
            CoverageStatus::Blocked,
            "js-required",
        )];

        let report = evaluate_compatibility_slo_v2(
            "top100.tsv",
            "forum.tsv",
            &top100_rows,
            &forum_rows,
            CompatibilitySloThresholds {
                readability_floor: 0.0,
                actionability_floor: 0.0,
                failure_quality_floor: 0.0,
            },
            None,
        );

        assert!(report.families.iter().any(|family| {
            family.family == "social-community"
                && family.rows_total == 2
                && family.eligible_rows == 1
        }));
        assert!(
            !report
                .families
                .iter()
                .any(|family| family.family == "reddit")
        );
    }

    #[test]
    fn compatibility_slo_v2_delta_is_deterministic_for_row_order() {
        let current_top100_a = vec![
            slo_row_with_family(
                CompatibilityCorpus::Top100,
                "one.example",
                "search-portal",
                Some("search-results"),
                1,
                2,
                CoverageExpectedPath::Adapter,
                CoverageStatus::Covered,
                "none",
            ),
            slo_row_with_family(
                CompatibilityCorpus::Top100,
                "two.example",
                "services-utility",
                Some("app-shell"),
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "auth-wall",
            ),
        ];
        let current_top100_b = vec![current_top100_a[1].clone(), current_top100_a[0].clone()];
        let current_forum_rows = vec![slo_row_with_family(
            CompatibilityCorpus::Forum,
            "forum.example",
            "legacy-forum",
            None,
            1,
            2,
            CoverageExpectedPath::Adapter,
            CoverageStatus::Covered,
            "none",
        )];
        let baseline_top100_rows = vec![slo_row_with_family(
            CompatibilityCorpus::Top100,
            "one.example",
            "search-portal",
            Some("search-results"),
            1,
            1,
            CoverageExpectedPath::Adapter,
            CoverageStatus::Covered,
            "none",
        )];
        let baseline_forum_rows = vec![slo_row_with_family(
            CompatibilityCorpus::Forum,
            "forum.example",
            "legacy-forum",
            None,
            1,
            1,
            CoverageExpectedPath::Adapter,
            CoverageStatus::Partial,
            "none",
        )];

        let report_a = evaluate_compatibility_slo_v2(
            "top100.tsv",
            "forum.tsv",
            &current_top100_a,
            &current_forum_rows,
            CompatibilitySloThresholds::default(),
            Some(crate::CompatibilitySloV2Baseline {
                top100_path: "baseline-top100.tsv",
                forum_path: "baseline-forum.tsv",
                top100_rows: &baseline_top100_rows,
                forum_rows: &baseline_forum_rows,
            }),
        );
        let report_b = evaluate_compatibility_slo_v2(
            "top100.tsv",
            "forum.tsv",
            &current_top100_b,
            &current_forum_rows,
            CompatibilitySloThresholds::default(),
            Some(crate::CompatibilitySloV2Baseline {
                top100_path: "baseline-top100.tsv",
                forum_path: "baseline-forum.tsv",
                top100_rows: &baseline_top100_rows,
                forum_rows: &baseline_forum_rows,
            }),
        );

        assert_eq!(report_a.delta, report_b.delta);
        assert_eq!(report_a.planning_inputs, report_b.planning_inputs);
    }

    #[test]
    fn run_with_args_reports_compatibility_slo_from_custom_matrices()
    -> Result<(), Box<dyn std::error::Error>> {
        let top100_path = unique_temp_file("compatibility-slo-top100");
        let forum_path = unique_temp_file("compatibility-slo-forum");
        fs::write(
            &top100_path,
            "# domain\tfamily\tprimary_intent\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nexample.org\tsearch-portal\tsearch-results\t1\t2\tdocs/fixture.html\tadapter\tcovered\tnone\nblocked.example\tsearch-portal\tapp-shell\t0\t0\tdocs/fixture.html\tfailure\tblocked\tauth-wall\n",
        )?;
        fs::write(
            &forum_path,
            "# domain\tfamily\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nforum.example\tlegacy-forum\t1\t2\tdocs/fixture.html\tadapter\tcovered\tnone\n",
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-slo".to_owned(),
                "--top100".to_owned(),
                top100_path.display().to_string(),
                "--forum".to_owned(),
                forum_path.display().to_string(),
                "--min-readability".to_owned(),
                "0.5".to_owned(),
                "--min-actionability".to_owned(),
                "0.5".to_owned(),
                "--min-failure-quality".to_owned(),
                "1.0".to_owned(),
            ],
            &mut stdin,
        )?;
        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-compatibility-slo-v1"));
                assert!(report.contains("result: pass"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }

        let _ = fs::remove_file(top100_path);
        let _ = fs::remove_file(forum_path);
        Ok(())
    }

    #[test]
    fn run_with_args_enforces_compatibility_slo_v2_gate() -> Result<(), Box<dyn std::error::Error>>
    {
        let top100_path = unique_temp_file("compatibility-slo-v2-top100");
        let forum_path = unique_temp_file("compatibility-slo-v2-forum");
        fs::write(
            &top100_path,
            "# domain\tfamily\tprimary_intent\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nexample.org\tsearch-portal\tsearch-results\t1\t0\tdocs/fixture.html\tadapter\tplanned\tnone\n",
        )?;
        fs::write(
            &forum_path,
            "# domain\tfamily\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nforum.example\tlegacy-forum\t1\t2\tdocs/fixture.html\tadapter\tcovered\tnone\n",
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-slo-v2".to_owned(),
                "--top100".to_owned(),
                top100_path.display().to_string(),
                "--forum".to_owned(),
                forum_path.display().to_string(),
                "--min-readability".to_owned(),
                "0.8".to_owned(),
                "--min-actionability".to_owned(),
                "0.5".to_owned(),
                "--min-failure-quality".to_owned(),
                "0.99".to_owned(),
            ],
            &mut stdin,
        );

        match output {
            Err(report) => {
                assert!(report.contains("index-compatibility-slo-v2"));
                assert!(report.contains("result: fail"));
                assert!(report.contains("planning_inputs:"));
            }
            other => {
                return Err(std::io::Error::other(format!(
                    "expected v2 gate failure, got {other:?}"
                ))
                .into());
            }
        }

        let _ = fs::remove_file(top100_path);
        let _ = fs::remove_file(forum_path);
        Ok(())
    }

    #[test]
    fn compatibility_backlog_ranking_is_deterministic() {
        let top100_rows = vec![
            backlog_row(
                CompatibilityCorpus::Top100,
                "alpha.example",
                "search-portal",
                Some("search-results"),
                1,
                1,
                CoverageExpectedPath::Generic,
                CoverageStatus::Partial,
                "none",
            ),
            backlog_row(
                CompatibilityCorpus::Top100,
                "blocked.example",
                "services-utility",
                Some("app-shell"),
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "auth-wall",
            ),
        ];
        let forum_rows = vec![backlog_row(
            CompatibilityCorpus::Forum,
            "forum.example",
            "legacy-forum",
            None,
            1,
            2,
            CoverageExpectedPath::Adapter,
            CoverageStatus::Covered,
            "none",
        )];
        let report_a =
            evaluate_compatibility_backlog("a.tsv", "b.tsv", 10, &top100_rows, &forum_rows);
        let mut top100_reordered = top100_rows.clone();
        top100_reordered.reverse();
        let report_b =
            evaluate_compatibility_backlog("a.tsv", "b.tsv", 10, &top100_reordered, &forum_rows);
        assert_eq!(report_a.ranked, report_b.ranked);
    }

    #[test]
    fn compatibility_backlog_priority_prefers_known_limit_none_and_low_tier() {
        let rows = vec![
            backlog_row(
                CompatibilityCorpus::Top100,
                "tier2.example",
                "search-portal",
                Some("search-results"),
                1,
                2,
                CoverageExpectedPath::Adapter,
                CoverageStatus::Covered,
                "none",
            ),
            backlog_row(
                CompatibilityCorpus::Top100,
                "tier0.example",
                "knowledge-reference",
                Some("article-or-reference"),
                1,
                0,
                CoverageExpectedPath::Generic,
                CoverageStatus::Planned,
                "none",
            ),
            backlog_row(
                CompatibilityCorpus::Top100,
                "blocked.example",
                "services-utility",
                Some("app-shell"),
                0,
                0,
                CoverageExpectedPath::Failure,
                CoverageStatus::Blocked,
                "auth-wall",
            ),
        ];
        let report = evaluate_compatibility_backlog("top.tsv", "forum.tsv", 3, &rows, &[]);
        assert_eq!(report.rows_total, 1);
        assert_eq!(report.ranked.len(), 1);
        assert_eq!(report.ranked[0].row.domain, "tier0.example");
    }

    #[test]
    fn compatibility_backlog_row_to_milestone_mapping_is_valid() {
        let readability = backlog_row(
            CompatibilityCorpus::Top100,
            "news.example",
            "knowledge-reference",
            Some("article-or-reference"),
            1,
            1,
            CoverageExpectedPath::Generic,
            CoverageStatus::Partial,
            "none",
        );
        let actionability = backlog_row(
            CompatibilityCorpus::Top100,
            "search.example",
            "search-portal",
            Some("search-results"),
            1,
            1,
            CoverageExpectedPath::Generic,
            CoverageStatus::Partial,
            "none",
        );
        let failure = backlog_row(
            CompatibilityCorpus::Top100,
            "blocked.example",
            "services-utility",
            Some("app-shell"),
            0,
            0,
            CoverageExpectedPath::Failure,
            CoverageStatus::Blocked,
            "auth-wall",
        );
        let app_shell = backlog_row(
            CompatibilityCorpus::Top100,
            "shell.example",
            "services-utility",
            Some("app-shell"),
            1,
            1,
            CoverageExpectedPath::Generic,
            CoverageStatus::Partial,
            "none",
        );

        assert_eq!(
            map_backlog_row_to_milestone(&readability).0,
            CompatibilityMilestone::M74
        );
        assert_eq!(
            map_backlog_row_to_milestone(&actionability).0,
            CompatibilityMilestone::M75
        );
        assert_eq!(
            map_backlog_row_to_milestone(&failure).0,
            CompatibilityMilestone::M76
        );
        assert_eq!(
            map_backlog_row_to_milestone(&app_shell).0,
            CompatibilityMilestone::M78
        );
    }

    #[test]
    fn compatibility_backlog_row_to_milestone_supports_social_community_alias_family() {
        let community = backlog_row(
            CompatibilityCorpus::Forum,
            "community.example",
            "social-community",
            Some("feed-or-thread"),
            1,
            1,
            CoverageExpectedPath::Generic,
            CoverageStatus::Partial,
            "none",
        );
        assert_eq!(
            map_backlog_row_to_milestone(&community).0,
            CompatibilityMilestone::M75
        );
    }

    #[test]
    fn parse_backlog_rows_canonicalize_reddit_and_generic_forum_families()
    -> Result<(), Box<dyn std::error::Error>> {
        let top100 = "# domain\tfamily\tprimary_intent\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nreddit.example\treddit\tfeed-or-thread\t1\t1\tdocs/fixture.html\tgeneric\tpartial\tnone\n";
        let forum = "# domain\tfamily\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nforum.example\tgeneric-forum\t1\t1\tdocs/fixture.html\tgeneric\tpartial\tnone\n";

        let parsed_top100 = parse_top100_backlog_rows(top100)?;
        let parsed_forum = parse_forum_backlog_rows(forum)?;

        assert_eq!(parsed_top100.len(), 1);
        assert_eq!(parsed_forum.len(), 1);
        assert_eq!(parsed_top100[0].family, "social-community");
        assert_eq!(parsed_forum[0].family, "social-community");
        Ok(())
    }

    #[test]
    fn run_with_args_reports_compatibility_backlog_from_custom_matrices()
    -> Result<(), Box<dyn std::error::Error>> {
        let top100_path = unique_temp_file("compatibility-backlog-top100");
        let forum_path = unique_temp_file("compatibility-backlog-forum");
        fs::write(
            &top100_path,
            "# domain\tfamily\tprimary_intent\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nalpha.example\tsearch-portal\tsearch-results\t1\t1\tdocs/fixture.html\tgeneric\tpartial\tnone\nblocked.example\tservices-utility\tapp-shell\t0\t0\tdocs/fixture.html\tfailure\tblocked\tauth-wall\n",
        )?;
        fs::write(
            &forum_path,
            "# domain\tfamily\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nforum.example\tlegacy-forum\t1\t2\tdocs/fixture.html\tadapter\tcovered\tnone\n",
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-backlog".to_owned(),
                "--top100".to_owned(),
                top100_path.display().to_string(),
                "--forum".to_owned(),
                forum_path.display().to_string(),
                "--top".to_owned(),
                "2".to_owned(),
            ],
            &mut stdin,
        )?;
        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-compatibility-backlog-v1"));
                assert!(report.contains("alpha.example"));
                assert!(!report.contains("blocked.example"));
                assert!(report.contains("M75"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }

        let _ = fs::remove_file(top100_path);
        let _ = fs::remove_file(forum_path);
        Ok(())
    }

    #[test]
    fn run_with_args_compatibility_pack_lint_reports_pass() -> Result<(), Box<dyn std::error::Error>>
    {
        let pack_path = unique_temp_file("compat-pack-lint");
        fs::write(
            &pack_path,
            r#"{
  "version": "index.pack/v1",
  "id": "lint-pack",
  "rules": [
    {
      "host": "example.org",
      "path_prefix": "/docs",
      "manifest": {
        "version": "index.idx/v1",
        "scope": "/docs",
        "content": { "main_selector": "main article" }
      }
    }
  ]
}"#,
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-pack".to_owned(),
                "lint".to_owned(),
                pack_path.display().to_string(),
                "https://example.org/docs/page".to_owned(),
            ],
            &mut stdin,
        )?;
        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-compat-pack-lint-v1"));
                assert!(report.contains("lint-pack"));
                assert!(report.contains("result: pass"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }
        let _ = fs::remove_file(pack_path);
        Ok(())
    }

    #[test]
    fn run_with_args_compatibility_pack_install_rejects_unknown_option() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-pack".to_owned(),
                "install".to_owned(),
                "docs/index-idx/examples/article.index.idx.json".to_owned(),
                "--remote".to_owned(),
            ],
            &mut stdin,
        );
        assert!(
            matches!(output, Err(error) if error.contains("unsupported compatibility-pack install option"))
        );
    }

    #[test]
    fn run_with_args_compatibility_pack_sign_and_verify_roundtrip()
    -> Result<(), Box<dyn std::error::Error>> {
        let pack_path = unique_temp_file("compat-pack-sign");
        fs::write(
            &pack_path,
            r#"{"version":"index.pack/v1","id":"sign-pack","rules":[]}"#,
        )?;
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let signed = run_with_args(
            vec![
                "compatibility-pack".to_owned(),
                "sign".to_owned(),
                pack_path.display().to_string(),
                "key-1".to_owned(),
                "secret-1".to_owned(),
            ],
            &mut stdin,
        )?;
        let signature = match signed {
            CliAction::Print(report) => report
                .lines()
                .find_map(|line| line.strip_prefix("signature: "))
                .ok_or_else(|| std::io::Error::other("missing signature line"))?
                .to_owned(),
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        };

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let verified = run_with_args(
            vec![
                "compatibility-pack".to_owned(),
                "verify".to_owned(),
                pack_path.display().to_string(),
                "key-1".to_owned(),
                "secret-1".to_owned(),
                signature,
            ],
            &mut stdin,
        )?;
        match verified {
            CliAction::Print(report) => {
                assert!(report.contains("index-compat-pack-verify-v1"));
                assert!(report.contains("result: pass"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }
        let _ = fs::remove_file(pack_path);
        Ok(())
    }

    #[test]
    fn run_with_args_reports_compatibility_live_variance_from_custom_files()
    -> Result<(), Box<dyn std::error::Error>> {
        let targets_path = unique_temp_file("compat-live-targets");
        let runs_path = unique_temp_file("compat-live-runs");
        fs::write(
            &targets_path,
            "# origin\tfamily\tenabled\nnews.ycombinator.com\tsocial-community\t1\nreddit.com\tsocial-community\t1\nexample.org\tknowledge-reference\t0\n",
        )?;
        fs::write(
            &runs_path,
            "# timestamp\torigin\toutcome\tlatency_ms\tblocked_class\n2026-05-13T00:00:00Z\tnews.ycombinator.com\tok\t120\t-\n2026-05-13T00:01:00Z\tnews.ycombinator.com\ttimeout\t-\t-\n2026-05-13T00:00:00Z\treddit.com\tblocked\t80\tbot-gate\n2026-05-13T00:02:00Z\texample.org\ttransient-error\t200\t-\n",
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-live-variance".to_owned(),
                "--targets".to_owned(),
                targets_path.display().to_string(),
                "--runs".to_owned(),
                runs_path.display().to_string(),
                "--window".to_owned(),
                "1".to_owned(),
            ],
            &mut stdin,
        )?;
        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-compatibility-live-variance-v1"));
                assert!(report.contains("news.ycombinator.com"));
                assert!(report.contains("reddit.com"));
                assert!(report.contains("blocked_class_columns"));
                assert!(report.contains("bot-gate"));
                assert!(report.contains("global_rates: flake=0.6667 timeout=0.3333"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }
        let _ = fs::remove_file(targets_path);
        let _ = fs::remove_file(runs_path);
        Ok(())
    }

    #[test]
    fn parse_live_variance_targets_rejects_invalid_rows() {
        let missing_fields = parse_live_variance_targets("only-origin\n");
        assert!(
            matches!(missing_fields, Err(error) if error.contains("expected at least 2 fields"))
        );

        let bad_enabled = parse_live_variance_targets("example.org\tsearch-portal\tmaybe\n");
        assert!(matches!(bad_enabled, Err(error) if error.contains("invalid enabled flag")));
    }

    #[test]
    fn parse_live_variance_rows_rejects_invalid_rows() {
        let missing_fields = parse_live_variance_rows("2026-01-01T00:00:00Z\texample.org\n");
        assert!(
            matches!(missing_fields, Err(error) if error.contains("expected at least 3 fields"))
        );

        let bad_outcome = parse_live_variance_rows("2026-01-01T00:00:00Z\texample.org\tunknown\n");
        assert!(
            matches!(bad_outcome, Err(error) if error.contains("invalid live-variance outcome"))
        );

        let bad_latency = parse_live_variance_rows("2026-01-01T00:00:00Z\texample.org\tok\tNaN\n");
        assert!(
            matches!(bad_latency, Err(error) if error.contains("invalid live-variance latency"))
        );

        let missing_origin = parse_live_variance_rows("2026-01-01T00:00:00Z\t\tok\t10\n");
        assert!(
            matches!(missing_origin, Err(error) if error.contains("missing timestamp or origin"))
        );
    }

    #[test]
    fn backlog_parsers_reject_invalid_rows() {
        let invalid_top100 = parse_top100_backlog_rows("domain\tfamily\tintent\n");
        assert!(matches!(invalid_top100, Err(error) if error.contains("expected 9 fields")));

        let missing_required_top100 = parse_top100_backlog_rows(
            "example.org\t\tsearch-results\t1\t1\tfixture\tgeneric\tpartial\tnone\n",
        );
        assert!(
            matches!(missing_required_top100, Err(error) if error.contains("missing required backlog fields"))
        );

        let invalid_forum = parse_forum_backlog_rows("domain\tfamily\n");
        assert!(matches!(invalid_forum, Err(error) if error.contains("expected 8 fields")));

        let missing_required_forum =
            parse_forum_backlog_rows("forum.example\t\t1\t1\tfixture\tgeneric\tpartial\tnone\n");
        assert!(
            matches!(missing_required_forum, Err(error) if error.contains("missing required backlog fields"))
        );
    }

    #[test]
    fn live_variance_report_text_handles_empty_blocked_classes() {
        let report = LiveVarianceReport {
            targets_path: "targets.tsv".to_owned(),
            runs_path: "runs.tsv".to_owned(),
            window: 3,
            rows_total: 1,
            enabled_targets: 1,
            total_samples: 1,
            global_flake_rate: 0.0,
            global_timeout_rate: 0.0,
            origins: vec![LiveVarianceOriginStats {
                origin: "example.org".to_owned(),
                family: "search-portal".to_owned(),
                samples: 1,
                ok: 1,
                timeout: 0,
                transient_error: 0,
                blocked: 0,
                flake_rate: 0.0,
                timeout_rate: 0.0,
                recent_flake_rate: 0.0,
                previous_flake_rate: 0.0,
                trend_delta: 0.0,
            }],
            blocked_class_counts: Vec::new(),
        };
        let text = report.to_text();
        assert!(text.contains("blocked_classes: none"));
        assert!(text.contains("origin_columns:"));
    }

    #[test]
    fn live_outcome_parse_supports_expected_aliases() {
        assert!(matches!(LiveOutcome::parse("ok"), Some(LiveOutcome::Ok)));
        assert!(matches!(
            LiveOutcome::parse("transient_error"),
            Some(LiveOutcome::TransientError)
        ));
        assert!(LiveOutcome::parse("mystery").is_none());
    }

    #[test]
    fn run_with_args_live_variance_reports_option_errors() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let missing_targets_value = run_with_args(
            vec![
                "compatibility-live-variance".to_owned(),
                "--targets".to_owned(),
            ],
            &mut stdin,
        );
        assert!(matches!(missing_targets_value, Ok(CliAction::Help(_))));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let invalid_window = run_with_args(
            vec![
                "compatibility-live-variance".to_owned(),
                "--window".to_owned(),
                "nan".to_owned(),
            ],
            &mut stdin,
        );
        assert!(matches!(invalid_window, Err(error) if error.contains("invalid --window value")));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let out_of_range_window = run_with_args(
            vec![
                "compatibility-live-variance".to_owned(),
                "--window".to_owned(),
                "0".to_owned(),
            ],
            &mut stdin,
        );
        assert!(matches!(out_of_range_window, Err(error) if error.contains("expected 1..=")));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let unsupported_option = run_with_args(
            vec![
                "compatibility-live-variance".to_owned(),
                "--nope".to_owned(),
            ],
            &mut stdin,
        );
        assert!(
            matches!(unsupported_option, Err(error) if error.contains("unsupported compatibility-live-variance option"))
        );
    }

    #[test]
    fn compatibility_slo_planning_inputs_cover_family_and_evidence_paths() {
        let thresholds = CompatibilitySloThresholds::default();
        let families = vec![
            FamilyCompatibilityScore {
                family: "app-shell".to_owned(),
                rows_total: 1,
                eligible_rows: 1,
                readable_rows: 1,
                actionable_rows: 0,
                failure_rows: 0,
                guarded_failure_rows: 0,
                readability_score: 1.0,
                actionability_score: 0.0,
                failure_quality_score: 1.0,
                thresholds,
                violations: vec!["actionability below floor".to_owned()],
                requires_non_failure_evidence: false,
            },
            FamilyCompatibilityScore {
                family: "search-portal".to_owned(),
                rows_total: 1,
                eligible_rows: 1,
                readable_rows: 0,
                actionable_rows: 0,
                failure_rows: 1,
                guarded_failure_rows: 0,
                readability_score: 0.0,
                actionability_score: 0.0,
                failure_quality_score: 0.0,
                thresholds,
                violations: vec![
                    "readability below floor".to_owned(),
                    "actionability below floor".to_owned(),
                    "failure-quality below floor".to_owned(),
                ],
                requires_non_failure_evidence: true,
            },
        ];
        let delta = Some(super::CompatibilitySloDeltaReport {
            readability_delta: -0.10,
            actionability_delta: -0.20,
            failure_quality_delta: -0.30,
            families: vec![CompatibilitySloFamilyDelta {
                family: "search-portal".to_owned(),
                readability_delta: -0.10,
                actionability_delta: -0.20,
                failure_quality_delta: -0.30,
            }],
        });
        let warnings = vec![
            "family search-portal missing non-failure evidence".to_owned(),
            "missing evidence".to_owned(),
        ];

        let inputs = compatibility_slo_planning_inputs(&families, delta.as_ref(), &warnings);
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M78: family-pack actionability gap"))
        );
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M74: readability gap"))
        );
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M75: actionability gap"))
        );
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M76: failure-quality gap"))
        );
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M73: evidence-gap backlog for family search-portal"))
        );
        assert!(
            inputs
                .iter()
                .any(|value| value == "M73: evidence-gap backlog")
        );
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M74: readability regression"))
        );
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M75: actionability regression"))
        );
        assert!(
            inputs
                .iter()
                .any(|value| value.contains("M76: failure-quality regression"))
        );
    }

    #[test]
    fn run_with_args_reports_recovery_plan_for_app_shell_target()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-recovery-plan".to_owned(),
                "chatgpt.com".to_owned(),
            ],
            &mut stdin,
        )?;
        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-compatibility-recovery-plan-v2"));
                assert!(report.contains("target: https://chatgpt.com"));
                assert!(report.contains("profile: app-shell"));
                assert!(report.contains(
                    "fallback_order: accessibility-snapshot -> rendered-dom -> static-dom"
                ));
                assert!(report.contains("fetching\t180"));
                assert!(report.contains("snapshotting\t45"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }
        Ok(())
    }

    #[test]
    fn run_with_args_auth_assist_diagnose_submit_classifies_token_failures()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "auth-assist".to_owned(),
                "diagnose-submit".to_owned(),
                "news.ycombinator.com/login".to_owned(),
                "403".to_owned(),
                "csrf".to_owned(),
                "token".to_owned(),
                "expired".to_owned(),
            ],
            &mut stdin,
        )?;
        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-auth-assist-diagnose-v1"));
                assert!(report.contains("target: https://news.ycombinator.com/login"));
                assert!(report.contains("class: csrf-token-mismatch"));
                assert!(report.contains("remediation: refresh session cookies"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }
        Ok(())
    }

    #[test]
    fn challenge_diagnose_report_classifies_blocked_flows() {
        let report = super::challenge_diagnose_report(
            "https://example.org",
            "<html><body>Attention required by Cloudflare</body></html>",
        );
        assert!(report.contains("index-challenge-diagnose-v1"));
        assert!(report.contains("class: bot-gate"));
        assert!(report.contains("reason: anti-bot challenge detected"));
    }

    #[test]
    fn auth_assist_diagnose_submit_defaults_to_https_and_reports_generic_failure() {
        let report = auth_assist_diagnose_submit("example.org/login", 500, "unexpected error");
        assert!(report.contains("target: https://example.org/login"));
        assert!(report.contains("class: generic-submit-failure"));
    }

    #[test]
    fn run_with_args_enforces_compatibility_recovery_gate_thresholds()
    -> Result<(), Box<dyn std::error::Error>> {
        let top100_path = unique_temp_file("compat-recovery-gate-top100");
        let forum_path = unique_temp_file("compat-recovery-gate-forum");
        let targets_path = unique_temp_file("compat-recovery-gate-targets");
        let runs_path = unique_temp_file("compat-recovery-gate-runs");
        fs::write(
            &top100_path,
            "# domain\tfamily\tprimary_intent\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nportal.example\tsearch-portal\tsearch-results\t1\t5\ta.html\tadapter\tcovered\tnone\nblocked.example\tservices-utility\tapp-shell\t0\t0\tb.html\tfailure\tblocked\tauth-wall\n",
        )?;
        fs::write(
            &forum_path,
            "# domain\tfamily\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nforum.example\tsocial-community\t1\t5\ta.html\tadapter\tcovered\tnone\n",
        )?;
        fs::write(
            &targets_path,
            "portal.example\tsearch-portal\t1\nforum.example\tsocial-community\t1\n",
        )?;
        fs::write(
            &runs_path,
            "2026-05-13T00:00:00Z\tportal.example\tok\t120\t-\n2026-05-13T00:01:00Z\tforum.example\ttimeout\t-\t-\n",
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "compatibility-recovery-gate".to_owned(),
                "--top100".to_owned(),
                top100_path.display().to_string(),
                "--forum".to_owned(),
                forum_path.display().to_string(),
                "--live-targets".to_owned(),
                targets_path.display().to_string(),
                "--live-runs".to_owned(),
                runs_path.display().to_string(),
                "--min-readability".to_owned(),
                "0.0".to_owned(),
                "--min-actionability".to_owned(),
                "0.0".to_owned(),
                "--min-failure-quality".to_owned(),
                "0.0".to_owned(),
                "--max-live-flake-rate".to_owned(),
                "0.80".to_owned(),
                "--max-live-timeout-rate".to_owned(),
                "0.60".to_owned(),
            ],
            &mut stdin,
        )?;

        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-compatibility-recovery-gate-v1"));
                assert!(report.contains("result: pass"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let failed = run_with_args(
            vec![
                "compatibility-recovery-gate".to_owned(),
                "--top100".to_owned(),
                top100_path.display().to_string(),
                "--forum".to_owned(),
                forum_path.display().to_string(),
                "--live-targets".to_owned(),
                targets_path.display().to_string(),
                "--live-runs".to_owned(),
                runs_path.display().to_string(),
                "--min-readability".to_owned(),
                "0.0".to_owned(),
                "--min-actionability".to_owned(),
                "0.0".to_owned(),
                "--min-failure-quality".to_owned(),
                "0.0".to_owned(),
                "--max-live-flake-rate".to_owned(),
                "0.20".to_owned(),
                "--max-live-timeout-rate".to_owned(),
                "0.20".to_owned(),
            ],
            &mut stdin,
        );
        assert!(failed.is_err());
        let error = failed.err().unwrap_or_default();
        assert!(error.contains("index-compatibility-recovery-gate-v1"));
        assert!(error.contains("result: fail"));

        let _ = fs::remove_file(top100_path);
        let _ = fs::remove_file(forum_path);
        let _ = fs::remove_file(targets_path);
        let _ = fs::remove_file(runs_path);
        Ok(())
    }

    #[test]
    fn manifest_candidate_discovery_prefers_well_known_then_header_then_html() {
        let page_url = "https://example.org/docs/article";
        let candidates = candidate_manifest_urls(
            page_url,
            r#"<html><head><link rel="index-manifest" href="/linked/index.idx"></head></html>"#,
            Some(r#"</header/index.idx>; rel="index-manifest""#),
        );
        assert_eq!(
            candidates,
            vec![
                "https://example.org/.well-known/index.idx".to_owned(),
                "https://example.org/header/index.idx".to_owned(),
                "https://example.org/linked/index.idx".to_owned(),
            ]
        );
    }

    #[test]
    fn artifact_runtime_emits_ordered_stage_progress_events()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let target = IndexUrl::parse("https://example.org/docs")?;
        fetcher.insert(
            target,
            "<html><head><title>Docs</title></head><body><main><p>Hello.</p></main></body></html>",
        );
        let mut cache = TransformedDocumentCache::default();
        let mut sequence = 0_u64;
        let store_root = unique_temp_file("artifact-stage-order");
        let store = index_capture::ArtifactStore::new(&store_root);
        let mut progress = Vec::new();
        let result = fetch_document_with_artifact_runtime(
            &fetcher,
            &mut cache,
            &mut sequence,
            &store,
            "https://example.org/docs",
            &mut |message| progress.push(message),
        )?;
        assert_eq!(
            result.visited_url.as_deref(),
            Some("https://example.org/docs")
        );
        assert_eq!(
            progress,
            vec![
                "queued https://example.org/docs".to_owned(),
                "fetching https://example.org/docs".to_owned(),
                "snapshotting https://example.org/docs".to_owned(),
                "parsing https://example.org/docs".to_owned(),
                "transforming https://example.org/docs".to_owned(),
                "scoring https://example.org/docs".to_owned(),
                "storing https://example.org/docs".to_owned(),
                "done https://example.org/docs".to_owned(),
            ]
        );
        let _ = fs::remove_dir_all(&store_root);
        Ok(())
    }

    #[test]
    fn artifact_runtime_applies_index_manifest_date_hints() -> Result<(), Box<dyn std::error::Error>>
    {
        let mut fetcher = MemoryFetcher::new();
        let page_url = IndexUrl::parse("https://example.org/docs/article")?;
        fetcher.insert(
            page_url.clone(),
            r#"<html><head><title>Docs</title></head><body><main><p>Updated: 2026-05-11T12:34:00Z</p></main></body></html>"#,
        );
        let manifest_url = IndexUrl::parse("https://example.org/.well-known/index.idx")?;
        fetcher.insert(
            manifest_url,
            r#"{
                "version":"index.idx/v1",
                "scope":"/docs",
                "fields":[{"name":"updated","label":"Updated"}],
                "dates":[{"field":"updated","style":"date"}]
            }"#,
        );

        let mut cache = TransformedDocumentCache::default();
        let mut sequence = 0_u64;
        let store_root = unique_temp_file("artifact-manifest-hints");
        let store = index_capture::ArtifactStore::new(&store_root);
        let mut progress = Vec::new();
        let result = fetch_document_with_artifact_runtime(
            &fetcher,
            &mut cache,
            &mut sequence,
            &store,
            page_url.as_str(),
            &mut |message| progress.push(message),
        )?;

        let rendered = render_document(&result.document, RenderOptions::default());
        assert!(rendered.contains("Updated: 2026-05-11"));
        assert!(!rendered.contains("2026-05-11T12:34:00Z"));
        let _ = fs::remove_dir_all(&store_root);
        Ok(())
    }

    #[test]
    fn artifact_runtime_ignores_invalid_manifest_deterministically()
    -> Result<(), Box<dyn std::error::Error>> {
        let page_url = IndexUrl::parse("https://example.org/docs/article")?;
        let html = r#"<html><head><title>Docs</title></head><body><main><p>Updated: 2026-05-11T12:34:00Z</p></main></body></html>"#;

        let mut fetcher_without_manifest = MemoryFetcher::new();
        fetcher_without_manifest.insert(page_url.clone(), html);

        let mut fetcher_with_invalid_manifest = MemoryFetcher::new();
        fetcher_with_invalid_manifest.insert(page_url.clone(), html);
        let manifest_url = IndexUrl::parse("https://example.org/.well-known/index.idx")?;
        fetcher_with_invalid_manifest.insert(
            manifest_url,
            r#"{"version":"index.idx/v1","scope":"docs","fields":[{"name":"updated","label":""}]}"#,
        );

        let mut cache_without_manifest = TransformedDocumentCache::default();
        let mut cache_with_invalid_manifest = TransformedDocumentCache::default();
        let mut sequence_a = 0_u64;
        let mut sequence_b = 0_u64;
        let store_root_a = unique_temp_file("artifact-manifest-invalid-a");
        let store_root_b = unique_temp_file("artifact-manifest-invalid-b");
        let store_a = index_capture::ArtifactStore::new(&store_root_a);
        let store_b = index_capture::ArtifactStore::new(&store_root_b);
        let mut progress = Vec::new();

        let without_manifest = fetch_document_with_artifact_runtime(
            &fetcher_without_manifest,
            &mut cache_without_manifest,
            &mut sequence_a,
            &store_a,
            page_url.as_str(),
            &mut |message| progress.push(message),
        )?;
        let with_invalid_manifest = fetch_document_with_artifact_runtime(
            &fetcher_with_invalid_manifest,
            &mut cache_with_invalid_manifest,
            &mut sequence_b,
            &store_b,
            page_url.as_str(),
            &mut |message| progress.push(message),
        )?;

        let rendered_without_manifest =
            render_document(&without_manifest.document, RenderOptions::default());
        let rendered_with_invalid_manifest =
            render_document(&with_invalid_manifest.document, RenderOptions::default());
        assert_eq!(rendered_without_manifest, rendered_with_invalid_manifest);

        let _ = fs::remove_dir_all(&store_root_a);
        let _ = fs::remove_dir_all(&store_root_b);
        Ok(())
    }

    #[test]
    fn artifact_runtime_reuses_cached_artifact_without_network_fetch()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let page_url = IndexUrl::parse("https://example.org/cache-hit")?;
        fetcher.insert(
            page_url.clone(),
            "<html><head><title>Cached</title></head><body><main><p>Hello from cache.</p></main></body></html>",
        );

        let mut cache = TransformedDocumentCache::default();
        let mut sequence = 0_u64;
        let store_root = unique_temp_file("artifact-cache-hit");
        let store = index_capture::ArtifactStore::new(&store_root);
        let mut first_progress = Vec::new();
        let first = fetch_document_with_artifact_runtime(
            &fetcher,
            &mut cache,
            &mut sequence,
            &store,
            page_url.as_str(),
            &mut |message| first_progress.push(message),
        )?;
        assert_eq!(first.visited_url.as_deref(), Some(page_url.as_str()));
        assert!(
            first_progress
                .iter()
                .any(|step| step.starts_with("fetching "))
        );

        let fetcher_without_network = MemoryFetcher::new();
        let mut second_progress = Vec::new();
        let second = fetch_document_with_artifact_runtime(
            &fetcher_without_network,
            &mut cache,
            &mut sequence,
            &store,
            page_url.as_str(),
            &mut |message| second_progress.push(message),
        )?;
        assert_eq!(second.visited_url.as_deref(), Some(page_url.as_str()));
        assert_eq!(
            second_progress,
            vec![
                format!("queued {}", page_url.as_str()),
                format!("transforming {}", page_url.as_str()),
                format!("scoring {}", page_url.as_str()),
                format!("done {}", page_url.as_str()),
            ]
        );

        let _ = fs::remove_dir_all(&store_root);
        Ok(())
    }

    #[test]
    fn artifact_runtime_reports_failed_stage_for_invalid_cached_artifact_url()
    -> Result<(), Box<dyn std::error::Error>> {
        let canonical = IndexUrl::parse("https://example.org/invalid-artifact")?;
        let final_url = IndexUrl::parse("https://example.org/rendered")?;
        let mut document = IndexDocument::titled("Invalid Artifact");
        document.push(IndexNode::Paragraph("body".to_owned()));
        let mut artifact = index_capture::IndexArtifact::from_document(
            &document,
            &canonical,
            &final_url,
            index_capture::ArtifactContext::LiveGet,
            0,
            60,
        )?;
        artifact.final_url = "://bad-final-url".to_owned();

        let store_root = unique_temp_file("artifact-invalid-final-url");
        let store = index_capture::ArtifactStore::new(&store_root);
        store.store(&artifact)?;

        let fetcher = MemoryFetcher::new();
        let mut cache = TransformedDocumentCache::default();
        let mut sequence = 0_u64;
        let mut progress = Vec::new();
        let result = fetch_document_with_artifact_runtime(
            &fetcher,
            &mut cache,
            &mut sequence,
            &store,
            canonical.as_str(),
            &mut |message| progress.push(message),
        );

        assert!(result.is_err());
        let error = result.err().unwrap_or_default();
        assert!(!error.is_empty());
        assert!(!progress.is_empty());

        let _ = fs::remove_dir_all(&store_root);
        Ok(())
    }

    #[test]
    fn submit_form_artifact_runtime_tracks_stages_and_stores_artifact()
    -> Result<(), Box<dyn std::error::Error>> {
        let form = Form {
            name: "login".to_owned(),
            method: "POST".to_owned(),
            action: "https://example.org/login".to_owned(),
            inputs: vec![Input {
                name: "user".to_owned(),
                kind: "text".to_owned(),
                value: Some("index".to_owned()),
                required: true,
            }],
            buttons: Vec::new(),
        };
        let submission = form.submit(None, &[])?;

        let mut fetcher = MemoryFetcher::new();
        fetcher.insert_form_response(
            &submission,
            Response {
                final_url: IndexUrl::parse("https://example.org/account")?,
                redirects: Vec::new(),
                mime_type: Some("text/html".to_owned()),
                body: "<html><head><title>Account</title></head><body><main><p>Submitted form.</p></main></body></html>".to_owned(),
            },
        );

        let store_root = unique_temp_file("submit-artifact-runtime");
        let store = index_capture::ArtifactStore::new(&store_root);
        let mut cache = TransformedDocumentCache::default();
        let mut sequence = 0_u64;
        let mut progress = Vec::new();
        let result = submit_form_with_artifact_runtime(
            &fetcher,
            &mut cache,
            &mut sequence,
            &store,
            &submission,
            &mut |message| progress.push(message),
        )?;
        assert_eq!(
            result.visited_url.as_deref(),
            Some("https://example.org/account")
        );
        assert_eq!(
            result.response_log.as_ref().map(|log| log.method.as_str()),
            Some("POST")
        );
        assert_eq!(
            progress,
            vec![
                "queued https://example.org/login".to_owned(),
                "fetching https://example.org/login".to_owned(),
                "snapshotting https://example.org/account".to_owned(),
                "parsing https://example.org/account".to_owned(),
                "transforming https://example.org/account".to_owned(),
                "scoring https://example.org/account".to_owned(),
                "storing https://example.org/account".to_owned(),
                "done https://example.org/account".to_owned(),
            ]
        );
        assert!(
            store
                .load(
                    &submission.action,
                    index_capture::ArtifactContext::LiveSubmit,
                )?
                .is_some()
        );

        let mut failing_progress = Vec::new();
        let empty_submitter = MemoryFetcher::new();
        let result = submit_form_with_artifact_runtime(
            &empty_submitter,
            &mut cache,
            &mut sequence,
            &store,
            &submission,
            &mut |message| failing_progress.push(message),
        );
        assert!(result.is_err());
        let error = result.err().unwrap_or_default();
        assert!(!error.is_empty());
        assert_eq!(
            failing_progress,
            vec![
                "queued https://example.org/login".to_owned(),
                "fetching https://example.org/login".to_owned(),
                "failed https://example.org/login".to_owned(),
            ]
        );

        let _ = fs::remove_dir_all(&store_root);
        Ok(())
    }

    #[test]
    fn parse_slo_floor_rejects_out_of_range_values() {
        let result = parse_slo_floor("readability", "1.2");
        assert!(result.is_err());
        let error = result.err().unwrap_or_default();
        assert!(error.contains("must be between 0.0 and 1.0"));
    }

    #[test]
    fn compatibility_slo_v2_report_reads_baselines_from_disk()
    -> Result<(), Box<dyn std::error::Error>> {
        let top100_path = unique_temp_file("compat-slo-v2-report-top100");
        let forum_path = unique_temp_file("compat-slo-v2-report-forum");
        let baseline_top100_path = unique_temp_file("compat-slo-v2-report-baseline-top100");
        let baseline_forum_path = unique_temp_file("compat-slo-v2-report-baseline-forum");

        fs::write(
            &top100_path,
            "# domain\tfamily\tprimary_intent\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nportal.example\tsearch-portal\tsearch-results\t2\t2\tdocs/fixture.html\tgeneric\tcovered\tnone\n",
        )?;
        fs::write(
            &forum_path,
            "# domain\tfamily\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nforum.example\tlegacy-forum\t2\t2\tdocs/fixture.html\tadapter\tcovered\tnone\n",
        )?;
        fs::write(
            &baseline_top100_path,
            "# domain\tfamily\tprimary_intent\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nportal.example\tsearch-portal\tsearch-results\t2\t2\tdocs/fixture.html\tgeneric\tcovered\tnone\n",
        )?;
        fs::write(
            &baseline_forum_path,
            "# domain\tfamily\tmin_tier\tcurrent_tier\tfixture\texpected_path\tstatus\tknown_limit\nforum.example\tlegacy-forum\t2\t2\tdocs/fixture.html\tadapter\tcovered\tnone\n",
        )?;

        let report = compatibility_slo_v2_report_from_paths(
            top100_path.to_str().ok_or("top100 path utf8")?,
            forum_path.to_str().ok_or("forum path utf8")?,
            CompatibilitySloThresholds::default(),
            Some(
                baseline_top100_path
                    .to_str()
                    .ok_or("baseline top100 path utf8")?,
            ),
            Some(
                baseline_forum_path
                    .to_str()
                    .ok_or("baseline forum path utf8")?,
            ),
        )?;

        assert!(report.baseline_top100_path.is_some());
        assert!(report.baseline_forum_path.is_some());

        let _ = fs::remove_file(top100_path);
        let _ = fs::remove_file(forum_path);
        let _ = fs::remove_file(baseline_top100_path);
        let _ = fs::remove_file(baseline_forum_path);
        Ok(())
    }

    #[test]
    fn run_with_args_idx_lint_reports_pass_for_valid_manifest()
    -> Result<(), Box<dyn std::error::Error>> {
        let manifest_path = unique_temp_file("idx-lint-valid");
        fs::write(
            &manifest_path,
            r#"{
                "version":"index.idx/v1",
                "scope":"/docs",
                "content":{"main_selector":"main article"},
                "regions":[{"role":"related","selector":"aside.related","collapsed":true}],
                "fields":[{"name":"updated","label":"Updated"}],
                "forms":[{"name":"search","selector":"form.search","note":"Public search"}],
                "dates":[{"field":"updated","style":"date"}]
            }"#,
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "idx".to_owned(),
                "lint".to_owned(),
                manifest_path.display().to_string(),
                "https://example.org/docs/page".to_owned(),
            ],
            &mut stdin,
        )?;

        match output {
            CliAction::Print(report) => {
                assert!(report.contains("index-idx-lint-v1"));
                assert!(report.contains("status: pass"));
                assert!(report.contains("checks: same-origin=pass scope=pass safety=pass"));
                assert!(report.contains("result: pass"));
            }
            other => {
                return Err(std::io::Error::other(format!("unexpected output: {other:?}")).into());
            }
        }

        let _ = fs::remove_file(&manifest_path);
        Ok(())
    }

    #[test]
    fn run_with_args_idx_lint_fails_closed_for_cross_origin_source()
    -> Result<(), Box<dyn std::error::Error>> {
        let manifest_path = unique_temp_file("idx-lint-cross-origin");
        fs::write(
            &manifest_path,
            r#"{"version":"index.idx/v1","scope":"/docs"}"#,
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let error = match run_with_args(
            vec![
                "idx".to_owned(),
                "lint".to_owned(),
                manifest_path.display().to_string(),
                "https://example.org/docs/page".to_owned(),
                "--source-url".to_owned(),
                "https://evil.example/index.idx".to_owned(),
            ],
            &mut stdin,
        ) {
            Err(error) => error,
            Ok(other) => {
                return Err(std::io::Error::other(format!(
                    "cross-origin lint should fail, got: {other:?}"
                ))
                .into());
            }
        };

        assert!(error.contains("index-idx-lint-v1"));
        assert!(error.contains("status: fail"));
        assert!(error.contains("same origin"));
        assert!(error.contains("result: fail"));

        let _ = fs::remove_file(&manifest_path);
        Ok(())
    }

    #[test]
    fn run_with_args_without_input_returns_default_start_page() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(Vec::<String>::new(), &mut stdin);
        assert!(matches!(output, Ok(CliAction::Tui { .. })));
        if let Ok(CliAction::Tui {
            document,
            profile,
            url_history,
        }) = output
        {
            assert_eq!(profile, ReaderProfile::Reader);
            assert!(url_history.is_empty());
            assert_eq!(document.title, "Index");
            assert!(
                document
                    .nodes
                    .iter()
                    .any(|node| matches!(node, index_core::IndexNode::Paragraph(text) if text.contains(":open <url>")))
            );
        }
    }

    #[test]
    fn default_start_page_contains_actionable_help() {
        let document = default_start_page();
        let rendered =
            index_renderer::render_document(&document, index_renderer::RenderOptions::default());

        assert!(rendered.contains("# Index"));
        assert!(rendered.contains(":open <id-or-url>"));
        assert!(rendered.contains("Example Domain -> https://example.org"));
    }

    fn image_node_count(nodes: &[IndexNode]) -> usize {
        nodes
            .iter()
            .map(|node| match node {
                IndexNode::Image { .. } => 1,
                IndexNode::Section { nodes, .. } => image_node_count(nodes),
                _ => 0,
            })
            .sum()
    }

    #[test]
    fn hydrate_document_images_renders_dithered_preview() {
        let bytes = include_bytes!("../../../assets/black-icon.png").to_vec();
        let mut document = index_core::IndexDocument::titled("Image Preview");
        document.push(IndexNode::Image {
            alt: "Index icon".to_owned(),
            src: Some("https://example.org/assets/icon.png".to_owned()),
        });

        let mut seen = Vec::new();
        let mut loader = |src: &str| -> Result<Vec<u8>, String> {
            seen.push(src.to_owned());
            Ok(bytes.clone())
        };

        let hydrated = hydrate_document_images_with_loader(document, &mut loader);
        let rendered =
            index_renderer::render_document(&hydrated, index_renderer::RenderOptions::default());

        assert_eq!(seen, vec!["https://example.org/assets/icon.png".to_owned()]);
        assert_eq!(image_node_count(&hydrated.nodes), 0);
        assert!(rendered.contains("bw-dither"));
        assert!(rendered.contains("image source -> https://example.org/assets/icon.png"));
        assert!(rendered.contains('█') || rendered.contains('▀') || rendered.contains('▄'));
    }

    #[test]
    fn hydrate_document_images_keeps_image_when_loader_fails() {
        let mut document = index_core::IndexDocument::titled("Fallback");
        document.push(IndexNode::Image {
            alt: "Missing".to_owned(),
            src: Some("https://example.org/missing.png".to_owned()),
        });

        let mut loader = |_src: &str| -> Result<Vec<u8>, String> { Err("fetch failed".to_owned()) };
        let hydrated = hydrate_document_images_with_loader(document, &mut loader);

        assert_eq!(image_node_count(&hydrated.nodes), 1);
    }

    #[test]
    fn hydrate_document_images_respects_preview_budget() {
        let bytes = include_bytes!("../../../assets/white-icon.png").to_vec();
        let mut document = index_core::IndexDocument::titled("Budget");
        for index in 0..(IMAGE_PREVIEW_MAX_IMAGES_PER_DOCUMENT + 2) {
            document.push(IndexNode::Image {
                alt: format!("image-{index}"),
                src: Some(format!("https://example.org/{index}.png")),
            });
        }

        let mut loader_calls = 0usize;
        let mut loader = |_src: &str| -> Result<Vec<u8>, String> {
            loader_calls += 1;
            Ok(bytes.clone())
        };

        let hydrated = hydrate_document_images_with_loader(document, &mut loader);

        assert_eq!(loader_calls, IMAGE_PREVIEW_MAX_IMAGES_PER_DOCUMENT);
        assert_eq!(image_node_count(&hydrated.nodes), 2);
    }

    #[test]
    fn hydrate_document_images_preserves_nested_sections_and_missing_sources() {
        let mut document = index_core::IndexDocument::titled("Nested");
        document.push(IndexNode::Section {
            role: SectionRole::Main,
            title: Some("Media".to_owned()),
            collapsed: false,
            nodes: vec![IndexNode::Image {
                alt: String::new(),
                src: None,
            }],
        });

        let mut loader_calls = 0usize;
        let mut loader = |_src: &str| -> Result<Vec<u8>, String> {
            loader_calls += 1;
            Ok(Vec::new())
        };
        let hydrated = hydrate_document_images_with_loader(document, &mut loader);

        assert_eq!(loader_calls, 0);
        assert!(matches!(
            hydrated.nodes.first(),
            Some(IndexNode::Section { nodes, .. })
                if matches!(nodes.first(), Some(IndexNode::Image { src: None, .. }))
        ));
    }

    #[test]
    fn runtime_stage_defaults_cover_default_profile_paths() {
        assert_eq!(RecoveryProfile::Default.as_str(), "default");
        assert_eq!(
            recovery_fallback_order(RecoveryProfile::Default)[0],
            "static-dom"
        );
        assert!(RuntimeStage::Scoring.default_timeout().is_some());
        assert!(RuntimeStage::Queued.default_timeout().is_none());
    }

    #[test]
    fn document_from_index_artifact_uncached_transforms_capture()
    -> Result<(), Box<dyn std::error::Error>> {
        let canonical = IndexUrl::parse("https://example.org/uncached-artifact")?;
        let mut document = IndexDocument::titled("Uncached");
        document.push(IndexNode::Paragraph("Body from artifact".to_owned()));
        let artifact = index_capture::IndexArtifact::from_document(
            &document,
            &canonical,
            &canonical,
            index_capture::ArtifactContext::LiveGet,
            0,
            60,
        )?;

        let rendered = render_document(
            &document_from_index_artifact_uncached(&artifact)?,
            RenderOptions::default(),
        );
        assert!(rendered.contains("Body from artifact"));
        Ok(())
    }

    #[test]
    fn refresh_live_get_artifact_stores_refreshed_snapshot()
    -> Result<(), Box<dyn std::error::Error>> {
        let canonical = IndexUrl::parse("https://example.org/refresh-live-get")?;
        let mut fetcher = MemoryFetcher::new();
        fetcher.insert(
            canonical.clone(),
            "<html><head><title>Refreshed</title></head><body><main><p>Fresh.</p></main></body></html>",
        );

        let store_root = unique_temp_file("refresh-live-get");
        let store = index_capture::ArtifactStore::new(&store_root);
        refresh_live_get_artifact(&fetcher, &store, &canonical)?;

        let stored = store
            .load(&canonical, index_capture::ArtifactContext::LiveGet)?
            .ok_or("missing stored artifact")?;
        let rendered = render_document(
            &document_from_index_artifact_uncached(&stored)?,
            RenderOptions::default(),
        );
        assert!(rendered.contains("Fresh."));

        let _ = fs::remove_dir_all(&store_root);
        Ok(())
    }

    #[test]
    fn run_save_and_batch_extract_helpers_cover_local_paths()
    -> Result<(), Box<dyn std::error::Error>> {
        let first = unique_temp_file("helper-save-input");
        let second = unique_temp_file("helper-batch-input");
        let output = unique_temp_file("helper-save-output");
        fs::write(
            &first,
            "<html><body><main><p>first</p></main></body></html>",
        )?;
        fs::write(
            &second,
            "<html><body><main><p>second</p></main></body></html>",
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let secure_fetcher = SecureFetcher::new(MemoryFetcher::new());
        let save = run_save_command(
            super::ExtractFormat::Markdown,
            &first.display().to_string(),
            &output.display().to_string(),
            &mut stdin,
            &secure_fetcher,
        )?;
        assert!(matches!(save, CliAction::Print(text) if text.contains("saved\tmarkdown")));
        assert!(fs::read_to_string(&output)?.contains("first"));

        let batch = run_batch_extract_command(
            super::ExtractFormat::Markdown,
            &[first.display().to_string(), second.display().to_string()],
        )?;
        assert!(matches!(
            batch,
            CliAction::Print(text) if text.contains("==> ") && text.contains("first") && text.contains("second")
        ));

        fs::remove_file(&first)?;
        fs::remove_file(&second)?;
        fs::remove_file(&output)?;
        Ok(())
    }

    #[test]
    fn run_with_args_returns_version() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(vec!["--version".to_owned()], &mut stdin);
        assert_eq!(output, Ok(CliAction::Version(version())));
    }

    #[test]
    fn run_with_args_returns_runtime_paths() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(vec!["--paths".to_owned()], &mut stdin);
        assert!(
            matches!(output, Ok(CliAction::Print(paths)) if paths.contains("config_dir=") && paths.contains("cache_dir=") && paths.contains("state_dir="))
        );
    }

    #[test]
    fn run_with_args_returns_telemetry_free_doctor_report() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(vec!["doctor".to_owned()], &mut stdin);

        assert!(matches!(
            output,
            Ok(CliAction::Print(report))
                if report.contains("index-doctor-v1")
                    && report.contains("telemetry=local-only")
                    && report.contains("network_probe=not-run")
                    && report.contains("support_report=review before sharing")
                    && !report.contains("https://")
        ));
    }

    #[test]
    fn doctor_report_redacts_home_paths_and_has_local_remediation() {
        let paths = RuntimePaths {
            config_dir: "/tmp/index-doctor-test/config".to_owned(),
            cache_dir: "/tmp/index-doctor-test/cache".to_owned(),
            state_dir: "/tmp/index-doctor-test/state".to_owned(),
        };
        let report = doctor_report(&paths);

        assert!(report.contains("config: missing; remediation: create with `mkdir -p"));
        assert!(report.contains("cache: missing; remediation: create with `mkdir -p"));
        assert!(report.contains("state: missing; remediation: create with `mkdir -p"));
        assert_eq!(
            redact_home_path("/definitely/not/home/index"),
            "/definitely/not/home/index"
        );
    }

    #[test]
    fn run_with_args_returns_benchmark_report_from_stdin() {
        let mut stdin = Cursor::new(
            b"<html><title>Bench</title><main><p>Fast enough.</p><a href=\"/ref\">Ref</a></main></html>"
                .to_vec(),
        );
        let output = run_with_args(vec!["--benchmark".to_owned(), "-".to_owned()], &mut stdin);

        assert!(matches!(output, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(report)) = output {
            assert!(report.contains("index-benchmark-v1"));
            assert!(report.contains("source: -"));
            assert!(report.contains("title: Bench"));
            assert!(report.contains("cache_entries: 1"));
            assert!(report.contains("cached_transform_micros:"));
        }
    }

    #[test]
    fn run_with_args_returns_benchmark_report_for_url() -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let url = IndexUrl::parse("https://example.org/bench")?;
        fetcher.insert(
            url,
            "<html><title>Remote Bench</title><main><p>Measured page.</p></main></html>",
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut stdin = Cursor::new(Vec::<u8>::new());

        let output = run_with_args_and_fetcher(
            vec!["--benchmark".to_owned(), "example.org/bench".to_owned()],
            &mut stdin,
            &fetcher,
        );

        assert!(matches!(output, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(report)) = output {
            assert!(report.contains("source: https://example.org/bench"));
            assert!(report.contains("title: Remote Bench"));
        }
        Ok(())
    }

    #[test]
    fn runtime_paths_display_is_stable() {
        let paths = RuntimePaths {
            config_dir: "/config/index".to_owned(),
            cache_dir: "/cache/index".to_owned(),
            state_dir: "/state/index".to_owned(),
        };
        assert_eq!(
            paths.display(),
            "config_dir=/config/index\ncache_dir=/cache/index\nstate_dir=/state/index"
        );
    }

    #[test]
    fn packaging_assets_describe_installed_command() {
        let readme = include_str!("../../../README.md");
        let man_page = include_str!("../../../docs/man/index.1");
        let bash_completion = include_str!("../../../completions/index.bash");
        let distro_notes = include_str!("../../../docs/packaging/DISTROS.md");
        let clean_install = include_str!("../../../docs/packaging/CLEAN_INSTALL.md");
        let package_manifest = include_str!("../../../packaging/package-manifest.tsv");
        let package_smoke = include_str!("../../../scripts/package-smoke.sh");
        let desktop_entry = include_str!("../../../packaging/index.desktop");
        let branding = include_str!("../../../docs/BRANDING.md");
        let white_icon = include_bytes!("../../../assets/white-icon.png");
        let black_icon = include_bytes!("../../../assets/black-icon.png");
        let packaged_banner = include_bytes!("../assets/white-banner.png");

        assert!(readme.contains("assets/white-banner.png"));
        assert!(man_page.contains(".B index"));
        assert!(man_page.contains("--paths"));
        assert!(bash_completion.contains("--version"));
        assert!(distro_notes.contains("/usr/bin/index"));
        assert!(distro_notes.contains("/usr/share/icons/hicolor/1024x1024/apps/index.png"));
        assert!(clean_install.contains("Clean Install Validation"));
        assert!(clean_install.contains("index --paths"));
        assert!(package_manifest.contains("share/man/man1/index.1"));
        assert!(package_manifest.contains("share/icons/hicolor/1024x1024/apps/index.png"));
        assert!(package_smoke.contains("scripts/check-package-manifest.sh"));
        assert!(package_smoke.contains("--plain"));
        assert!(desktop_entry.contains("Icon=index"));
        assert!(desktop_entry.contains("Terminal=true"));
        assert!(branding.contains("JetBrainsMono Nerd Font Mono"));
        assert!(white_icon.len() > 1024);
        assert!(black_icon.len() > 1024);
        assert!(packaged_banner.len() > 1024);
    }

    #[test]
    fn artifact_runtime_assets_define_schema_policy_and_commands() {
        let readme = include_str!("../../../README.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let architecture = include_str!("../../../docs/ARCHITECTURE.md");
        let artifact_runtime = include_str!("../../../docs/ARTIFACT_RUNTIME.md");
        let adr = include_str!("../../../docs/adr/0068-canonical-artifact-runtime.md");
        let rfc = include_str!("../../../docs/rfc/0006-canonical-artifact-runtime.md");

        assert!(readme.contains("index artifact inspect https://example.org/docs"));
        assert!(readme.contains("docs/ARTIFACT_RUNTIME.md"));
        assert!(roadmap.contains("Milestone 68 — Canonical Artifact Runtime"));
        assert!(roadmap.contains("**Status:** complete."));
        assert!(changelog.contains("Canonical artifact runtime milestone (M68)"));
        assert!(architecture.contains("index-artifact-v1"));
        assert!(artifact_runtime.contains("index-artifact-v1"));
        assert!(artifact_runtime.contains("stale-while-revalidate"));
        assert!(adr.contains("ArtifactStore"));
        assert!(rfc.contains("canonical local artifact"));
    }

    #[test]
    fn async_stage_assets_define_runtime_progress_contract() {
        let readme = include_str!("../../../README.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let architecture = include_str!("../../../docs/ARCHITECTURE.md");
        let async_stages = include_str!("../../../docs/ASYNC_STAGES.md");
        let adr = include_str!("../../../docs/adr/0069-async-stage-execution-model.md");
        let rfc = include_str!("../../../docs/rfc/0007-async-stage-execution-model.md");

        assert!(readme.contains("docs/ASYNC_STAGES.md"));
        assert!(roadmap.contains("Milestone 69 — Async Stage Execution Model"));
        assert!(roadmap.contains("**Status:** complete."));
        assert!(changelog.contains("Async stage execution milestone (M69)"));
        assert!(architecture.contains("queued"));
        assert!(async_stages.contains("snapshotting"));
        assert!(async_stages.contains("Stage budgets"));
        assert!(adr.contains("StageFlow"));
        assert!(rfc.contains("queued"));
    }

    #[test]
    fn index_idx_protocol_assets_define_safe_manifest_contract() {
        let readme = include_str!("../../../README.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let architecture = include_str!("../../../docs/ARCHITECTURE.md");
        let protocol = include_str!("../../../docs/INDEX_IDX_PROTOCOL.md");
        let adr = include_str!("../../../docs/adr/0070-index-idx-protocol-v1.md");
        let rfc = include_str!("../../../docs/rfc/0008-index-idx-protocol-v1.md");

        assert!(readme.contains("docs/INDEX_IDX_PROTOCOL.md"));
        assert!(roadmap.contains("Milestone 70 — `index.idx` Protocol v1"));
        assert!(roadmap.contains("**Status:** complete."));
        assert!(changelog.contains("`index.idx` protocol milestone (M70)"));
        assert!(architecture.contains("optional same-origin manifest"));
        assert!(protocol.contains("index.idx/v1"));
        assert!(protocol.contains("fail closed"));
        assert!(adr.contains("selector syntax/complexity checks"));
        assert!(rfc.contains("Discovery order"));
    }

    #[test]
    fn site_family_pack_assets_define_escalation_and_fixture_contract() {
        let readme = include_str!("../../../README.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let catalog = include_str!("../../../docs/COVERAGE_CATALOG.md");
        let discipline = include_str!("../../../docs/ADAPTER_DISCIPLINE.md");
        let packs = include_str!("../../../docs/SITE_FAMILY_PACKS.md");
        let adr = include_str!("../../../docs/adr/0071-site-family-compatibility-packs.md");
        let rfc = include_str!("../../../docs/rfc/0009-site-family-compatibility-packs.md");

        assert!(readme.contains("docs/SITE_FAMILY_PACKS.md"));
        assert!(roadmap.contains("Milestone 71 — Site-Family Compatibility Packs"));
        assert!(roadmap.contains("**Status:** complete."));
        assert!(changelog.contains("Site-family compatibility packs milestone (M71)"));
        assert!(catalog.contains("fixtures/adapters/family-forum.html"));
        assert!(catalog.contains("fixtures/adapters/family-portal.html"));
        assert!(discipline.contains("Family-pack coverage was evaluated"));
        assert!(packs.contains("family-pack.docs"));
        assert!(adr.contains("family-pack.portal"));
        assert!(rfc.contains("shared heuristics"));
    }

    #[test]
    fn alpha_hardening_assets_cover_release_paths() {
        let alpha = include_str!("../../../docs/ALPHA.md");
        let known_limits = include_str!("../../../docs/KNOWN_LIMITS.md");
        let dogfooding = include_str!("../../../docs/DOGFOODING.md");
        let smoke = include_str!("../../../scripts/alpha-smoke.sh");

        assert!(alpha.contains("Alpha Release Checklist"));
        assert!(alpha.contains("alpha-blocker/network"));
        assert!(alpha.contains("fixture, capture artifact, diagnostic document"));
        assert!(known_limits.contains("Index does not claim full web compatibility"));
        assert!(known_limits.contains("No workflow should upload browsing state automatically"));
        assert!(dogfooding.contains("Daily Notes Template"));
        assert!(dogfooding.contains("crates/index-transformer/tests/fixtures/wiki-reference.html"));
        assert!(smoke.contains("--plain"));
        assert!(smoke.contains("--extract markdown"));
        assert!(smoke.contains("capture --preview --redact"));
        assert!(smoke.contains("adapter check"));
        assert!(smoke.contains("shelf save"));
        assert!(smoke.contains("INDEX_ALPHA_SMOKE_LIVE_URL"));
        assert!(smoke.contains("INDEX_ALPHA_SMOKE_TUI"));
    }

    #[test]
    fn dogfooding_corpus_assets_are_offline_and_tiered() {
        let corpus_doc = include_str!("../../../docs/dogfooding/CORPUS.md");
        let corpus = include_str!("../../../docs/dogfooding/corpus.tsv");
        let live = include_str!("../../../docs/dogfooding/live-smoke.tsv");
        let notes = include_str!("../../../docs/dogfooding/daily-notes-template.md");
        let checker = include_str!("../../../scripts/check-dogfooding-corpus.sh");

        assert!(corpus_doc.contains("without"));
        assert!(corpus_doc.contains("fetching any URL"));
        assert!(corpus.contains("mdn-adapter"));
        assert!(corpus.contains("crates/index-transformer/tests/fixtures/wiki-reference.html"));
        assert!(live.contains("https://developer.mozilla.org/"));
        assert!(live.contains("expected_path"));
        assert!(notes.contains("reproduction command"));
        assert!(checker.contains("validate_committed"));
        assert!(checker.contains("validate_live"));
        assert!(checker.contains("http://*|https://*"));
    }

    #[test]
    fn security_review_assets_cover_abuse_boundaries() {
        let review = include_str!("../../../docs/SECURITY_REVIEW.md");
        let abuse = include_str!("../../../docs/ABUSE_CASES.md");
        let catalog = include_str!("../../../docs/security-abuse-cases.tsv");
        let checker = include_str!("../../../scripts/check-security-review.sh");
        let adr = include_str!("../../../docs/adr/0048-security-review-abuse-cases.md");

        assert!(review.contains("Review Checklist"));
        assert!(review.contains("Terminal Escape Audit"));
        assert!(review.contains("Capture Privacy Audit"));
        assert!(review.contains("Network Request Expansion Audit"));
        assert!(review.contains("cargo deny check"));
        assert!(abuse.contains("deferred-blocker"));
        assert!(catalog.contains("terminal-control\tterminal"));
        assert!(catalog.contains("capture-secret-url\tcapture"));
        assert!(catalog.contains("unsafe-url-scheme\turl"));
        assert!(catalog.contains("unsupported-content-type\tnetwork"));
        assert!(catalog.contains("remote-local-read\tlocal-file"));
        assert!(catalog.contains("dependency-advisory\tdependency"));
        assert!(catalog.contains("ai-private-content\tai"));
        assert!(checker.contains("make audit"));
        assert!(checker.contains("cargo deny check"));
        assert!(adr.contains("make security-review"));
    }

    #[test]
    fn compatibility_assets_cover_accessibility_fallbacks() {
        let compatibility = include_str!("../../../docs/COMPATIBILITY.md");
        let validation = include_str!("../../../docs/COMPATIBILITY_VALIDATION.md");
        let accessibility = include_str!("../../../docs/ACCESSIBILITY.md");
        let checker = include_str!("../../../scripts/check-compatibility.sh");
        let adr = include_str!("../../../docs/adr/0049-compatibility-accessibility-validation.md");

        assert!(compatibility.contains("Capability Levels"));
        assert!(validation.contains("True-color"));
        assert!(validation.contains("Monochrome"));
        assert!(validation.contains("Plain monospace font"));
        assert!(validation.contains("No-animation mode"));
        assert!(validation.contains("Narrow-Terminal Policy"));
        assert!(accessibility.contains("prompt is always visible"));
        assert!(accessibility.contains("screen-reader"));
        assert!(checker.contains("JetBrainsMono Nerd Font Mono"));
        assert!(checker.contains("plain-glyph"));
        assert!(adr.contains("plain ASCII labels"));
    }

    #[test]
    fn release_candidate_assets_cover_checksums_and_rollback() {
        let release = include_str!("../../../docs/RELEASE.md");
        let notes = include_str!("../../../docs/RELEASE_NOTES_TEMPLATE.md");
        let script = include_str!("../../../scripts/release-candidate-dry-run.sh");
        let adr = include_str!("../../../docs/adr/0050-release-candidate-process.md");

        assert!(release.contains("Release Candidate Checklist"));
        assert!(release.contains("Versioning Policy"));
        assert!(release.contains("Artifact Checksums"));
        assert!(release.contains("Rollback Notes"));
        assert!(release.contains("Post-Release Smoke Checklist"));
        assert!(notes.contains("Support level: alpha / beta / stable"));
        assert!(notes.contains("Known limits"));
        assert!(script.contains("scripts/package-smoke.sh"));
        assert!(script.contains("dist/SHA256SUMS"));
        assert!(script.contains("sha256sum"));
        assert!(adr.contains("does not tag, sign, upload, or publish"));
    }

    #[test]
    fn doctor_assets_document_local_only_support_reports() {
        let diagnostics = include_str!("../../../docs/DIAGNOSTICS.md");
        let doctor = include_str!("../../../docs/DOCTOR.md");
        let adr = include_str!("../../../docs/adr/0051-telemetry-free-operational-diagnostics.md");

        assert!(diagnostics.contains("index doctor"));
        assert!(doctor.contains("index-doctor-v1"));
        assert!(doctor.contains("telemetry=local-only"));
        assert!(doctor.contains("network_probe=not-run"));
        assert!(doctor.contains("Support Report Checklist"));
        assert!(adr.contains("does not perform network IO"));
    }

    #[test]
    fn beta_readiness_assets_define_narrow_support_gate() {
        let beta = include_str!("../../../docs/BETA.md");
        let report = include_str!("../../../docs/BETA_READINESS_REPORT.md");
        let robustness = include_str!("../../../docs/ROBUSTNESS.md");
        let robustness_report = include_str!("../../../docs/ROBUSTNESS_READINESS_REPORT.md");
        let checker = include_str!("../../../scripts/check-beta-readiness.sh");
        let adr = include_str!("../../../docs/adr/0052-beta-readiness-gate.md");

        assert!(beta.contains("Beta Readiness Checklist"));
        assert!(beta.contains("No known critical security or data-loss issue remains open"));
        assert!(beta.contains("unsupported pages fail usefully"));
        assert!(robustness.contains("Robustness Gate Checklist"));
        assert!(robustness.contains("No-Hang Runtime Policy"));
        assert!(robustness.contains("Deterministic Failure Policy"));
        assert!(robustness_report.contains("Robustness gate is blocked"));
        assert!(report.contains("Alpha Blocker Burn-Down"));
        assert!(report.contains("Performance baseline"));
        assert!(checker.contains("scripts/check-dogfooding-corpus.sh"));
        assert!(checker.contains("scripts/check-top100-corpus.sh"));
        assert!(checker.contains("scripts/check-security-review.sh"));
        assert!(checker.contains("scripts/check-compatibility-slo.sh"));
        assert!(checker.contains("scripts/check-compatibility-slo-v2.sh"));
        assert!(checker.contains("scripts/check-package-manifest.sh"));
        assert!(checker.contains("scripts/check-robustness-gate.sh"));
        assert!(adr.contains("Beta support promises stay narrow"));
    }

    #[test]
    fn stable_readiness_assets_require_beta_cycle_evidence() {
        let stable = include_str!("../../../docs/STABLE.md");
        let report = include_str!("../../../docs/STABLE_READINESS_REPORT.md");
        let checker = include_str!("../../../scripts/check-stable-readiness.sh");
        let adr = include_str!("../../../docs/adr/0053-stable-readiness-policy.md");

        assert!(stable.contains("Stable Support Policy"));
        assert!(stable.contains("Maintenance and Patch Policy"));
        assert!(stable.contains("Compatibility Promise"));
        assert!(stable.contains("Deprecation Policy"));
        assert!(stable.contains("Security Response Process"));
        assert!(stable.contains("Long-Term Fixture Stewardship"));
        assert!(report.contains("Stable release is blocked"));
        assert!(report.contains("Fixture Stewardship Notes"));
        assert!(report.contains("Compatibility SLO"));
        assert!(checker.contains("scripts/check-beta-readiness.sh"));
        assert!(checker.contains("scripts/check-robustness-gate.sh"));
        assert!(checker.contains("scripts/check-coverage-catalog.sh"));
        assert!(checker.contains("scripts/check-dogfooding-corpus.sh"));
        assert!(checker.contains("scripts/check-top100-corpus.sh"));
        assert!(adr.contains("repeated release evidence"));
    }

    #[test]
    fn robustness_gate_assets_define_local_reliability_command() {
        let policy = include_str!("../../../docs/ROBUSTNESS.md");
        let report = include_str!("../../../docs/ROBUSTNESS_READINESS_REPORT.md");
        let checker = include_str!("../../../scripts/check-robustness-gate.sh");
        let makefile = include_str!("../../../Makefile");
        let release = include_str!("../../../docs/RELEASE.md");
        let adr = include_str!("../../../docs/adr/0067-robustness-gate-v1.md");

        assert!(policy.contains("Robustness Gate Checklist"));
        assert!(policy.contains("No-Hang Runtime Policy"));
        assert!(policy.contains("Deterministic Failure Policy"));
        assert!(report.contains("Robustness gate is blocked"));
        assert!(checker.contains("scripts/check-forum-corpus.sh"));
        assert!(checker.contains("scripts/check-top100-corpus.sh"));
        assert!(checker.contains("scripts/check-security-review.sh"));
        assert!(checker.contains("scripts/check-compatibility-slo.sh"));
        assert!(checker.contains("scripts/alpha-smoke.sh"));
        assert!(makefile.contains("robustness-gate"));
        assert!(release.contains("Run `make robustness-gate`."));
        assert!(adr.contains("make robustness-gate"));
    }

    #[test]
    fn compatibility_slo_assets_define_release_blocking_gate() {
        let policy = include_str!("../../../docs/COMPATIBILITY_SLO.md");
        let report = include_str!("../../../docs/COMPATIBILITY_SLO_REPORT.md");
        let checker = include_str!("../../../scripts/check-compatibility-slo.sh");
        let makefile = include_str!("../../../Makefile");
        let beta_checker = include_str!("../../../scripts/check-beta-readiness.sh");
        let adr = include_str!("../../../docs/adr/0072-compatibility-slo-gate-v1.md");
        let rfc = include_str!("../../../docs/rfc/0010-compatibility-slo-gate-v1.md");

        assert!(policy.contains("Compatibility SLO v1"));
        assert!(policy.contains("Readability"));
        assert!(policy.contains("Actionability"));
        assert!(policy.contains("Failure Quality"));
        assert!(report.contains("Compatibility SLO gate is blocked"));
        assert!(checker.contains("index-cli -- compatibility-slo"));
        assert!(checker.contains("docs/top100-corpus/matrix.tsv"));
        assert!(checker.contains("docs/forum-corpus/matrix.tsv"));
        assert!(makefile.contains("compatibility-slo"));
        assert!(beta_checker.contains("scripts/check-compatibility-slo.sh"));
        assert!(adr.contains("readability floor"));
        assert!(rfc.contains("SLO Metrics"));
    }

    #[test]
    fn compatibility_backlog_assets_define_ranked_burndown_queue() {
        let burndown = include_str!("../../../docs/COMPATIBILITY_BURNDOWN.md");
        let checker = include_str!("../../../scripts/check-compatibility-backlog.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0073-compatibility-backlog-queue-v1.md");
        let rfc = include_str!("../../../docs/rfc/0011-compatibility-backlog-queue-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");

        assert!(burndown.contains("Compatibility Burn-Down Scoreboard"));
        assert!(burndown.contains("index compatibility-backlog"));
        assert!(burndown.contains("Top N Queue"));
        assert!(checker.contains("compatibility-backlog"));
        assert!(checker.contains("cmp -s"));
        assert!(makefile.contains("compatibility-backlog"));
        assert!(adr.contains("deterministic ranking queue"));
        assert!(rfc.contains("Priority Policy"));
        assert!(roadmap.contains("Milestone 73 — SLO Burn-Down Queue v1"));
    }

    #[test]
    fn readability_v2_assets_define_fixture_and_verification_contract() {
        let policy = include_str!("../../../docs/COMPATIBILITY_READABILITY_V2.md");
        let checker = include_str!("../../../scripts/check-readability-lift-v2.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0074-generic-readability-lift-v2.md");
        let rfc = include_str!("../../../docs/rfc/0012-generic-readability-lift-v2.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let catalog = include_str!("../../../docs/COVERAGE_CATALOG.md");

        assert!(policy.contains("Generic Readability Lift v2"));
        assert!(policy.contains("make readability-lift-v2"));
        assert!(checker.contains("cargo test -p index-dom"));
        assert!(checker.contains("static_reader readability_v2"));
        assert!(makefile.contains("readability-lift-v2"));
        assert!(adr.contains("deterministic readability-v2 policy"));
        assert!(rfc.contains("root-selection scoring"));
        assert!(roadmap.contains("Milestone 74 — Generic Readability Lift v2"));
        assert!(changelog.contains("Generic readability lift milestone (M74)"));
        assert!(catalog.contains("readability-v2-article.html"));
    }

    #[test]
    fn actionability_v2_assets_define_link_form_and_forum_lift_contract() {
        let policy = include_str!("../../../docs/COMPATIBILITY_ACTIONABILITY_V2.md");
        let checker = include_str!("../../../scripts/check-actionability-lift-v2.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0075-generic-actionability-lift-v2.md");
        let rfc = include_str!("../../../docs/rfc/0013-generic-actionability-lift-v2.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Generic Actionability Lift v2"));
        assert!(policy.contains("forum \"next steps\""));
        assert!(checker.contains("emit_links_ranks_navigation_and_result_links"));
        assert!(checker.contains("form_submission_uses_default_field_values_and_allows_overrides"));
        assert!(makefile.contains("actionability-lift-v2"));
        assert!(adr.contains("actionability-v2 policy"));
        assert!(rfc.contains("deterministic link ordering/deduplication"));
        assert!(roadmap.contains("Milestone 75 — Generic Actionability Lift v2"));
        assert!(changelog.contains("Generic actionability lift milestone (M75)"));
    }

    #[test]
    fn failure_quality_v3_assets_define_blocked_flow_guardrail_contract() {
        let policy = include_str!("../../../docs/FAILURE_QUALITY_V3.md");
        let checker = include_str!("../../../scripts/check-failure-quality-v3.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0076-failure-quality-hardening-v3.md");
        let rfc = include_str!("../../../docs/rfc/0014-failure-quality-hardening-v3.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Failure Quality Hardening v3"));
        assert!(policy.contains("policy-blocked"));
        assert!(checker.contains("blocked_flow_guardrails_cover_required_classes"));
        assert!(checker.contains("unsupported_page_shape_never_looks_successful"));
        assert!(checker.contains("blocked_top100_document_emits_remediation_and_capture_guidance"));
        assert!(makefile.contains("failure-quality-v3"));
        assert!(adr.contains("blocked-flow taxonomy coverage"));
        assert!(rfc.contains("silent-success"));
        assert!(roadmap.contains("Milestone 76 — Failure Quality Hardening v3"));
        assert!(changelog.contains("Failure-quality hardening milestone (M76)"));
    }

    #[test]
    fn index_idx_adoption_v1_assets_define_lint_and_template_contract() {
        let adoption = include_str!("../../../docs/INDEX_IDX_ADOPTION.md");
        let publisher = include_str!("../../../docs/INDEX_IDX_PUBLISHER_GUIDE.md");
        let checker = include_str!("../../../scripts/check-index-idx-adoption-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0077-index-idx-adoption-toolkit-v1.md");
        let rfc = include_str!("../../../docs/rfc/0015-index-idx-adoption-toolkit-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let template = include_str!("../../../docs/index-idx/TEMPLATE.index.idx.json");
        let article_example =
            include_str!("../../../docs/index-idx/examples/article.index.idx.json");

        assert!(adoption.contains("`index.idx` Adoption Toolkit v1"));
        assert!(adoption.contains("index idx lint"));
        assert!(publisher.contains("Safe Selector Guidance"));
        assert!(checker.contains("idx lint"));
        assert!(checker.contains("cross-origin lint failure"));
        assert!(makefile.contains("index-idx-adoption-v1"));
        assert!(adr.contains("index idx lint"));
        assert!(rfc.contains("local validation command"));
        assert!(roadmap.contains("Milestone 77 — `index.idx` Adoption Toolkit v1"));
        assert!(changelog.contains("`index.idx` adoption toolkit milestone (M77)"));
        assert!(template.contains("\"version\": \"index.idx/v1\""));
        assert!(article_example.contains("\"main_selector\": \"main article\""));
    }

    #[test]
    fn family_pack_expansion_v2_assets_define_confidence_and_fixture_contract() {
        let policy = include_str!("../../../docs/FAMILY_PACK_EXPANSION_V2.md");
        let pack_policy = include_str!("../../../docs/SITE_FAMILY_PACKS.md");
        let checker = include_str!("../../../scripts/check-family-pack-expansion-v2.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0078-family-pack-expansion-v2.md");
        let rfc = include_str!("../../../docs/rfc/0016-family-pack-expansion-v2.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let catalog = include_str!("../../../docs/COVERAGE_CATALOG.md");

        assert!(policy.contains("Family-Pack Expansion v2"));
        assert!(policy.contains("family-pack.app-shell"));
        assert!(policy.contains("family-pack.commerce-cards"));
        assert!(policy.contains("family-pack.mixed-media"));
        assert!(pack_policy.contains("Coverage targets"));
        assert!(checker.contains("compatibility_pack_weak_signals_fall_back_to_generic"));
        assert!(checker.contains("compatibility_pack_fixtures_cover_major_families"));
        assert!(makefile.contains("family-pack-expansion-v2"));
        assert!(adr.contains("minimum-confidence fallback"));
        assert!(rfc.contains("confidence scoring"));
        assert!(roadmap.contains("Milestone 78 — Family-Pack Expansion v2"));
        assert!(changelog.contains("Family-pack expansion milestone (M78)"));
        assert!(catalog.contains("family-app-shell.html"));
        assert!(catalog.contains("family-commerce-cards.html"));
        assert!(catalog.contains("family-mixed-media.html"));
    }

    #[test]
    fn compatibility_slo_v2_assets_define_family_threshold_and_delta_gate() {
        let policy = include_str!("../../../docs/COMPATIBILITY_SLO_V2.md");
        let report = include_str!("../../../docs/COMPATIBILITY_SLO_V2_REPORT.md");
        let checker = include_str!("../../../scripts/check-compatibility-slo-v2.sh");
        let makefile = include_str!("../../../Makefile");
        let release = include_str!("../../../docs/RELEASE.md");
        let adr = include_str!("../../../docs/adr/0079-compatibility-slo-gate-v2.md");
        let rfc = include_str!("../../../docs/rfc/0017-compatibility-slo-gate-v2.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let beta_checker = include_str!("../../../scripts/check-beta-readiness.sh");

        assert!(policy.contains("Compatibility SLO v2"));
        assert!(policy.contains("per-family"));
        assert!(report.contains("Compatibility SLO v2 gate is blocked"));
        assert!(checker.contains("compatibility-slo-v2"));
        assert!(checker.contains("baseline-top100"));
        assert!(checker.contains("cmp -s"));
        assert!(makefile.contains("compatibility-slo-v2"));
        assert!(release.contains("make compatibility-slo-v2"));
        assert!(adr.contains("per-family threshold enforcement"));
        assert!(rfc.contains("deterministic baseline delta"));
        assert!(roadmap.contains("Milestone 79 — Compatibility SLO Gate v2"));
        assert!(changelog.contains("Compatibility SLO gate milestone (M79)"));
        assert!(beta_checker.contains("check-compatibility-slo-v2.sh"));
    }

    #[test]
    fn compatibility_pack_runtime_v1_assets_define_safe_runtime_contract() {
        let policy = include_str!("../../../docs/COMPATIBILITY_PACK_RUNTIME_V1.md");
        let checker = include_str!("../../../scripts/check-compatibility-pack-runtime-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0080-compatibility-pack-runtime-v1.md");
        let rfc = include_str!("../../../docs/rfc/0018-compatibility-pack-runtime-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let readme = include_str!("../../../README.md");
        let builtin_pack = include_str!("../assets/compat/default.pack.json");

        assert!(policy.contains("Compatibility Pack Runtime v1"));
        assert!(policy.contains("user` > `trusted` > `built-in` > generic"));
        assert!(checker.contains("compatibility-pack lint"));
        assert!(checker.contains("compat_pack_runtime::tests"));
        assert!(makefile.contains("compatibility-pack-runtime-v1"));
        assert!(adr.contains("index.pack/v1"));
        assert!(rfc.contains("compatibility-pack"));
        assert!(roadmap.contains("Milestone 80 — Compatibility Pack Runtime v1"));
        assert!(
            roadmap.contains(
                "## Milestone 80 — Compatibility Pack Runtime v1\n\n**Status:** complete."
            )
        );
        assert!(changelog.contains("Compatibility pack runtime milestone (M80)"));
        assert!(readme.contains("index compatibility-pack lint"));
        assert!(builtin_pack.contains("\"version\": \"index.pack/v1\""));
    }

    #[test]
    fn compat_lab_bootstrap_v1_assets_define_standalone_artifact_workflow() {
        let policy = include_str!("../../../docs/COMPAT_LAB_BOOTSTRAP_V1.md");
        let checker = include_str!("../../../scripts/check-compat-lab-bootstrap-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0081-index-compat-lab-bootstrap-v1.md");
        let rfc = include_str!("../../../docs/rfc/0019-index-compat-lab-bootstrap-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let readme = include_str!("../../../README.md");

        assert!(policy.contains("Compatibility Lab Bootstrap v1"));
        assert!(policy.contains("index-compat-lab"));
        assert!(checker.contains("index-compat-lab -- ingest"));
        assert!(checker.contains("index-compat-lab -- scaffold"));
        assert!(makefile.contains("compat-lab-bootstrap-v1"));
        assert!(adr.contains("index-compat-lab"));
        assert!(rfc.contains("standalone crate"));
        assert!(roadmap.contains("Milestone 81 — `index-compat-lab` Crate Bootstrap"));
        assert!(roadmap.contains(
            "## Milestone 81 — `index-compat-lab` Crate Bootstrap\n\n**Status:** complete."
        ));
        assert!(changelog.contains("bootstrap milestone (M81)"));
        assert!(readme.contains("index-compat-lab"));
    }

    #[test]
    fn compat_rule_synthesis_v1_assets_define_safety_lint_contract() {
        let policy = include_str!("../../../docs/COMPAT_RULE_SYNTHESIS_V1.md");
        let checker = include_str!("../../../scripts/check-compat-rule-synthesis-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0082-compat-rule-synthesis-and-lint-v1.md");
        let rfc = include_str!("../../../docs/rfc/0020-compat-rule-synthesis-and-lint-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Rule Synthesis and Lint v1"));
        assert!(policy.contains("merge-overrides"));
        assert!(checker.contains("index-compat-lab -- synthesize"));
        assert!(checker.contains("index-compat-lab -- lint"));
        assert!(checker.contains("merge-overrides"));
        assert!(makefile.contains("compat-rule-synthesis-v1"));
        assert!(adr.contains("unsafe selectors"));
        assert!(rfc.contains("deterministic override merge"));
        assert!(roadmap.contains("Milestone 82 — Rule Synthesis and Safety Linting"));
        assert!(roadmap.contains(
            "## Milestone 82 — Rule Synthesis and Safety Linting\n\n**Status:** complete."
        ));
        assert!(changelog.contains("Rule synthesis and safety linting milestone (M82)"));
    }

    #[test]
    fn compat_pack_trust_v1_assets_define_signing_and_lifecycle_contract() {
        let policy = include_str!("../../../docs/COMPAT_PACK_TRUST_V1.md");
        let checker = include_str!("../../../scripts/check-compat-pack-trust-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0083-compat-pack-trust-signing-v1.md");
        let rfc = include_str!("../../../docs/rfc/0021-compat-pack-trust-signing-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Trust and Registry v1"));
        assert!(policy.contains("sign"));
        assert!(policy.contains("verify"));
        assert!(checker.contains("compatibility-pack sign"));
        assert!(checker.contains("compatibility-pack verify"));
        assert!(makefile.contains("compat-pack-trust-v1"));
        assert!(adr.contains("provenance"));
        assert!(rfc.contains("signing/verification"));
        assert!(roadmap.contains("Milestone 83 — Trust, Signing, and Pack Registry v1"));
        assert!(roadmap.contains(
            "## Milestone 83 — Trust, Signing, and Pack Registry v1\n\n**Status:** complete."
        ));
        assert!(changelog.contains("Trust/signing/registry milestone (M83)"));
    }

    #[test]
    fn compat_pack_hotswap_v1_assets_define_rollback_contract() {
        let policy = include_str!("../../../docs/COMPAT_PACK_HOTSWAP_V1.md");
        let checker = include_str!("../../../scripts/check-compat-pack-hotswap-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0084-compat-pack-hotswap-rollback-v1.md");
        let rfc = include_str!("../../../docs/rfc/0022-compat-pack-hotswap-rollback-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Hot-Swap and Rollback v1"));
        assert!(policy.contains("rollback"));
        assert!(checker.contains("rollback_restores_previous_pack_snapshot"));
        assert!(checker.contains("compatibility-pack inspect"));
        assert!(makefile.contains("compat-pack-hotswap-v1"));
        assert!(adr.contains("rollback snapshots"));
        assert!(rfc.contains("rollback"));
        assert!(roadmap.contains("Milestone 84 — Runtime Hot-Swap and Rollback"));
        assert!(
            roadmap.contains(
                "## Milestone 84 — Runtime Hot-Swap and Rollback\n\n**Status:** complete."
            )
        );
        assert!(changelog.contains("Hot-swap and rollback milestone (M84)"));
    }

    #[test]
    fn compat_pack_ci_v1_assets_define_composed_canary_gate() {
        let policy = include_str!("../../../docs/COMPAT_PACK_CI_V1.md");
        let checker = include_str!("../../../scripts/check-compat-pack-ci-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0085-compat-pack-ci-canary-v1.md");
        let rfc = include_str!("../../../docs/rfc/0023-compat-pack-ci-canary-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("CI and Canary Gate v1"));
        assert!(checker.contains("check-compatibility-pack-runtime-v1.sh"));
        assert!(checker.contains("check-compatibility-slo-v2.sh"));
        assert!(checker.contains("check-compatibility-backlog.sh"));
        assert!(makefile.contains("compat-pack-ci-v1"));
        assert!(adr.contains("canary"));
        assert!(rfc.contains("pack quality"));
        assert!(roadmap.contains("Milestone 85 — Compatibility Pack CI and Canary Gate"));
        assert!(roadmap.contains(
            "## Milestone 85 — Compatibility Pack CI and Canary Gate\n\n**Status:** complete."
        ));
        assert!(changelog.contains("Compatibility pack CI/canary milestone (M85)"));
    }

    #[test]
    fn compat_no_binary_release_v1_assets_define_data_plane_release_contract() {
        let policy = include_str!("../../../docs/COMPAT_DATA_RELEASE_V1.md");
        let checker = include_str!("../../../scripts/check-compat-no-binary-release-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0086-compatibility-data-release-workflow-v1.md");
        let rfc = include_str!("../../../docs/rfc/0024-compatibility-data-release-workflow-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Data-Only Release Workflow v1"));
        assert!(policy.contains("Engine vs Data Boundary"));
        assert!(checker.contains("check-compat-pack-ci-v1.sh"));
        assert!(checker.contains("index-compat-lab -- scaffold"));
        assert!(checker.contains("compatibility-pack verify"));
        assert!(makefile.contains("compat-no-binary-release-v1"));
        assert!(adr.contains("no-binary-release workflow"));
        assert!(rfc.contains("compatibility-only releases"));
        assert!(roadmap.contains("Milestone 86 — No-Binary-Release Compatibility Workflow v1"));
        assert!(roadmap.contains(
            "## Milestone 86 — No-Binary-Release Compatibility Workflow v1\n\n**Status:** complete."
        ));
        assert!(changelog.contains("No-binary-release compatibility workflow milestone (M86)"));
    }

    #[test]
    fn live_variance_v1_assets_define_opt_in_instability_contract() {
        let policy = include_str!("../../../docs/LIVE_VARIANCE_V1.md");
        let checker = include_str!("../../../scripts/check-live-variance-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0087-live-variance-harness-v1.md");
        let rfc = include_str!("../../../docs/rfc/0025-live-variance-harness-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let targets = include_str!("../../../docs/compat-live/targets.tsv");
        let runs = include_str!("../../../docs/compat-live/runs.tsv");

        assert!(policy.contains("Live Variance Harness v1"));
        assert!(policy.contains("local CI must remain deterministic offline"));
        assert!(checker.contains("compatibility-live-variance"));
        assert!(checker.contains("cmp -s"));
        assert!(makefile.contains("live-variance-v1"));
        assert!(adr.contains("live variance harness"));
        assert!(rfc.contains("live variance harness"));
        assert!(roadmap.contains("Milestone 87 — Live Variance Harness v1"));
        assert!(
            roadmap.contains("## Milestone 87 — Live Variance Harness v1\n\n**Status:** complete.")
        );
        assert!(changelog.contains("Live variance harness milestone (M87)"));
        assert!(targets.contains("news.ycombinator.com"));
        assert!(runs.contains("timestamp_utc"));
    }

    #[test]
    fn app_shell_recovery_v2_assets_define_bounded_profiled_fallback_contract() {
        let policy = include_str!("../../../docs/APP_SHELL_RECOVERY_V2.md");
        let checker = include_str!("../../../scripts/check-app-shell-recovery-v2.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0088-js-app-shell-recovery-v2.md");
        let rfc = include_str!("../../../docs/rfc/0026-js-app-shell-recovery-v2.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");
        let async_stages = include_str!("../../../docs/ASYNC_STAGES.md");

        assert!(policy.contains("JS/App-Shell Recovery v2"));
        assert!(policy.contains("static-dom"));
        assert!(checker.contains("compatibility-recovery-plan"));
        assert!(checker.contains("profile: app-shell"));
        assert!(makefile.contains("app-shell-recovery-v2"));
        assert!(adr.contains("recovery profiles"));
        assert!(rfc.contains("recovery profiles"));
        assert!(roadmap.contains("Milestone 88 — JS/App-Shell Recovery v2"));
        assert!(
            roadmap.contains("## Milestone 88 — JS/App-Shell Recovery v2\n\n**Status:** complete.")
        );
        assert!(changelog.contains("JS/app-shell recovery milestone (M88)"));
        assert!(async_stages.contains("App-shell profile"));
    }

    #[test]
    fn auth_assist_v1_assets_define_local_session_diagnostics_contract() {
        let policy = include_str!("../../../docs/AUTH_ASSIST_V1.md");
        let checker = include_str!("../../../scripts/check-auth-assist-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0089-session-aware-auth-assist-v1.md");
        let rfc = include_str!("../../../docs/rfc/0027-session-aware-auth-assist-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Session-Aware Auth Assist v1"));
        assert!(policy.contains("local-only"));
        assert!(checker.contains("auth-assist import"));
        assert!(checker.contains("diagnose-submit"));
        assert!(makefile.contains("auth-assist-v1"));
        assert!(adr.contains("auth-assist"));
        assert!(rfc.contains("auth-assist"));
        assert!(roadmap.contains("Milestone 89 — Session-Aware Auth Assist v1"));
        assert!(
            roadmap.contains(
                "## Milestone 89 — Session-Aware Auth Assist v1\n\n**Status:** complete."
            )
        );
        assert!(changelog.contains("Session-aware auth assist milestone (M89)"));
    }

    #[test]
    fn challenge_failure_ux_v1_assets_define_blocked_flow_reason_contract() {
        let policy = include_str!("../../../docs/CHALLENGE_FAILURE_UX_V1.md");
        let checker = include_str!("../../../scripts/check-challenge-failure-ux-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0090-challenge-aware-failure-ux-v1.md");
        let rfc = include_str!("../../../docs/rfc/0028-challenge-aware-failure-ux-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Challenge-Aware Failure UX v1"));
        assert!(policy.contains("captcha"));
        assert!(checker.contains("challenge-diagnose"));
        assert!(checker.contains("class: bot-gate"));
        assert!(makefile.contains("challenge-failure-ux-v1"));
        assert!(adr.contains("blocked-flow"));
        assert!(rfc.contains("challenge"));
        assert!(roadmap.contains("Milestone 90 — Challenge-Aware Failure UX v1"));
        assert!(
            roadmap.contains(
                "## Milestone 90 — Challenge-Aware Failure UX v1\n\n**Status:** complete."
            )
        );
        assert!(changelog.contains("Challenge-aware failure UX milestone (M90)"));
    }

    #[test]
    fn layout_fidelity_v3_assets_define_spacing_and_code_fidelity_contract() {
        let policy = include_str!("../../../docs/LAYOUT_FIDELITY_V3.md");
        let checker = include_str!("../../../scripts/check-layout-fidelity-v3.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0091-layout-fidelity-lift-v3.md");
        let rfc = include_str!("../../../docs/rfc/0029-layout-fidelity-lift-v3.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Layout Fidelity Lift v3"));
        assert!(policy.contains("pre`/`code"));
        assert!(checker.contains("preserves_pre_and_code_text_whitespace"));
        assert!(checker.contains("renders_layout_spacers_as_extra_blank_lines"));
        assert!(makefile.contains("layout-fidelity-v3"));
        assert!(adr.contains("spacing rhythm"));
        assert!(rfc.contains("card/grid"));
        assert!(roadmap.contains("Milestone 91 — Layout Fidelity Lift v3"));
        assert!(
            roadmap.contains("## Milestone 91 — Layout Fidelity Lift v3\n\n**Status:** complete.")
        );
        assert!(changelog.contains("Layout fidelity lift milestone (M91)"));
    }

    #[test]
    fn international_text_v2_assets_define_multilingual_guardrail_contract() {
        let policy = include_str!("../../../docs/INTERNATIONAL_TEXT_V2.md");
        let checker = include_str!("../../../scripts/check-international-text-v2.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0092-international-text-robustness-v2.md");
        let rfc = include_str!("../../../docs/rfc/0030-international-text-robustness-v2.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("International Text Robustness v2"));
        assert!(policy.contains("mixed-direction"));
        assert!(checker.contains("cjk_text_wraps_by_display_width_without_overflowing_columns"));
        assert!(
            checker.contains("shelf_search_matches_non_english_titles_notes_tags_and_headings")
        );
        assert!(makefile.contains("international-text-v2"));
        assert!(adr.contains("multilingual"));
        assert!(rfc.contains("multilingual"));
        assert!(roadmap.contains("Milestone 92 — International Text Robustness v2"));
        assert!(roadmap.contains(
            "## Milestone 92 — International Text Robustness v2\n\n**Status:** complete."
        ));
        assert!(changelog.contains("International text robustness milestone (M92)"));
    }

    #[test]
    fn structured_data_recovery_v1_assets_define_metadata_recovery_contract() {
        let policy = include_str!("../../../docs/STRUCTURED_DATA_RECOVERY_V1.md");
        let checker = include_str!("../../../scripts/check-structured-data-recovery-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0093-structured-data-recovery-v1.md");
        let rfc = include_str!("../../../docs/rfc/0031-structured-data-recovery-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Structured Data Recovery v1"));
        assert!(policy.contains("metadata"));
        assert!(checker.contains("extracts_metadata_and_resolves_relative_urls_against_base"));
        assert!(checker.contains("apply_metadata_sets_document_metadata"));
        assert!(makefile.contains("structured-data-recovery-v1"));
        assert!(adr.contains("metadata"));
        assert!(rfc.contains("metadata"));
        assert!(roadmap.contains("Milestone 93 — Structured Data Recovery v1"));
        assert!(
            roadmap
                .contains("## Milestone 93 — Structured Data Recovery v1\n\n**Status:** complete.")
        );
        assert!(changelog.contains("Structured data recovery milestone (M93)"));
    }

    #[test]
    fn compat_data_plane_v2_assets_define_synthesis_quality_and_strict_lint_contract() {
        let policy = include_str!("../../../docs/COMPAT_DATA_PLANE_V2.md");
        let checker = include_str!("../../../scripts/check-compat-data-plane-v2.sh");
        let makefile = include_str!("../../../Makefile");
        let adr = include_str!("../../../docs/adr/0094-compatibility-data-plane-expansion-v2.md");
        let rfc = include_str!("../../../docs/rfc/0032-compatibility-data-plane-expansion-v2.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Compatibility Data-Plane Expansion v2"));
        assert!(checker.contains("quality: score="));
        assert!(checker.contains("unsupported manifest version"));
        assert!(makefile.contains("compat-data-plane-v2"));
        assert!(adr.contains("quality scoring"));
        assert!(rfc.contains("synthesis quality"));
        assert!(roadmap.contains("Milestone 94 — Compatibility Data-Plane Expansion v2"));
        assert!(roadmap.contains(
            "## Milestone 94 — Compatibility Data-Plane Expansion v2\n\n**Status:** complete."
        ));
        assert!(changelog.contains("Compatibility data-plane expansion milestone (M94)"));
    }

    #[test]
    fn compatibility_recovery_gate_v1_assets_define_composed_release_evidence_contract() {
        let policy = include_str!("../../../docs/COMPATIBILITY_RECOVERY_GATE_V1.md");
        let report = include_str!("../../../docs/COMPATIBILITY_RECOVERY_GATE_V1_REPORT.md");
        let checker = include_str!("../../../scripts/check-compatibility-recovery-gate-v1.sh");
        let makefile = include_str!("../../../Makefile");
        let release = include_str!("../../../docs/RELEASE.md");
        let beta = include_str!("../../../docs/BETA.md");
        let beta_report = include_str!("../../../docs/BETA_READINESS_REPORT.md");
        let stable = include_str!("../../../docs/STABLE.md");
        let stable_report = include_str!("../../../docs/STABLE_READINESS_REPORT.md");
        let adr = include_str!("../../../docs/adr/0095-compatibility-recovery-gate-v1.md");
        let rfc = include_str!("../../../docs/rfc/0033-compatibility-recovery-gate-v1.md");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(policy.contains("Compatibility Recovery Gate v1"));
        assert!(report.contains("gate result"));
        assert!(checker.contains("check-live-variance-v1.sh"));
        assert!(checker.contains("check-compat-data-plane-v2.sh"));
        assert!(makefile.contains("compatibility-recovery-gate"));
        assert!(release.contains("make compatibility-recovery-gate"));
        assert!(beta.contains("make compatibility-recovery-gate"));
        assert!(beta_report.contains("Compatibility recovery gate"));
        assert!(stable.contains("make compatibility-recovery-gate"));
        assert!(stable_report.contains("Compatibility recovery gate"));
        assert!(adr.contains("SLO-v2 and live-variance"));
        assert!(rfc.contains("compatibility-recovery-gate"));
        assert!(roadmap.contains("Milestone 95 — Compatibility Recovery Gate v1"));
        assert!(
            roadmap.contains(
                "## Milestone 95 — Compatibility Recovery Gate v1\n\n**Status:** complete."
            )
        );
        assert!(changelog.contains("Compatibility recovery gate milestone (M95)"));
    }

    #[test]
    fn greatness_gate_assets_define_strict_quality_contract() {
        let makefile = include_str!("../../../Makefile");
        let readme = include_str!("../../../README.md");
        let gates = include_str!("../../../docs/GREATNESS_GATES.md");
        let performance = include_str!("../../../scripts/check-performance-great.sh");
        let security = include_str!("../../../scripts/check-security-best.sh");
        let ux = include_str!("../../../scripts/check-ux-great.sh");
        let readiness = include_str!("../../../scripts/check-readiness-great.sh");
        let beta = include_str!("../../../docs/BETA_READINESS_REPORT.md");
        let stable = include_str!("../../../docs/STABLE_READINESS_REPORT.md");
        let robustness = include_str!("../../../docs/ROBUSTNESS_READINESS_REPORT.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(makefile.contains("performance-great"));
        assert!(makefile.contains("security-best"));
        assert!(makefile.contains("ux-great"));
        assert!(makefile.contains("readiness-great"));
        assert!(readme.contains("index quickstart"));
        assert!(readme.contains("make readiness-great"));
        assert!(gates.contains("Greatness Gates"));
        assert!(performance.contains("--benchmark examples/sample.html"));
        assert!(performance.contains("PERFORMANCE_GREAT_MAX_FIRST_TRANSFORM_MICROS"));
        assert!(security.contains("cargo deny check"));
        assert!(security.contains("capture_redacts_credentials_cookies_and_private_fields"));
        assert!(ux.contains("index quickstart"));
        assert!(ux.contains("default_start_page_contains_actionable_help"));
        assert!(readiness.contains("--min-readability 1.0"));
        assert!(readiness.contains("check-stable-readiness.sh"));
        assert!(beta.contains("Greatness gate"));
        assert!(stable.contains("Greatness gate"));
        assert!(robustness.contains("Greatness gate"));
        assert!(changelog.contains("quality gates"));
    }

    #[test]
    fn one_zero_gates_assets_define_release_closure_contract() {
        let makefile = include_str!("../../../Makefile");
        let readme = include_str!("../../../README.md");
        let release = include_str!("../../../docs/RELEASE.md");
        let stable_report = include_str!("../../../docs/STABLE_READINESS_REPORT.md");
        let closure = include_str!("../../../docs/SECURITY_CLOSURE_V1.md");
        let perf = include_str!("../../../docs/PERFORMANCE_CAPACITY_V1.md");
        let ux = include_str!("../../../docs/UX_INTERACTION_V1.md");
        let operability = include_str!("../../../docs/OPERABILITY_RELEASE_EVIDENCE_V1.md");
        let contract = include_str!("../../../docs/CONTRACT_FREEZE_V1.md");
        let rc = include_str!("../../../docs/ONE_ZERO_RC_V1.md");
        let bundle_doc = include_str!("../../../docs/RELEASE_EVIDENCE_BUNDLE_V1.md");
        let script_security = include_str!("../../../scripts/check-security-closure-gate-v1.sh");
        let script_perf = include_str!("../../../scripts/check-performance-capacity-gate-v1.sh");
        let script_ux = include_str!("../../../scripts/check-ux-interaction-gate-v1.sh");
        let script_operability =
            include_str!("../../../scripts/check-operability-release-evidence-gate-v1.sh");
        let script_contract = include_str!("../../../scripts/check-contract-freeze-gate-v1.sh");
        let script_rc = include_str!("../../../scripts/check-release-1-0-gate-v1.sh");
        let script_bundle = include_str!("../../../scripts/generate-release-evidence-bundle-v1.sh");
        let roadmap = include_str!("../../../ROADMAP.md");
        let changelog = include_str!("../../../CHANGELOG.md");

        assert!(makefile.contains("security-closure-v1"));
        assert!(makefile.contains("performance-capacity-v1"));
        assert!(makefile.contains("ux-interaction-v1"));
        assert!(makefile.contains("operability-evidence-v1"));
        assert!(makefile.contains("contract-freeze-v1"));
        assert!(makefile.contains("release-1-0-gate-v1"));
        assert!(readme.contains("make release-1-0-gate-v1"));
        assert!(release.contains("make release-1-0-gate-v1"));
        assert!(stable_report.contains("Security closure gate v1"));
        assert!(stable_report.contains("1.0 release gate v1"));
        assert!(closure.contains("Milestone: M96"));
        assert!(perf.contains("Milestone: M97"));
        assert!(ux.contains("Milestone: M98"));
        assert!(operability.contains("Milestone: M99"));
        assert!(contract.contains("Milestone: M100"));
        assert!(rc.contains("Milestone: M101"));
        assert!(bundle_doc.contains("BUNDLE_MANIFEST.tsv"));
        assert!(script_security.contains("security-open-risks.tsv"));
        assert!(script_perf.contains("check-performance-great.sh"));
        assert!(script_ux.contains("check-ux-great.sh"));
        assert!(script_operability.contains("generate-release-evidence-bundle-v1.sh"));
        assert!(script_contract.contains("index.idx/v1"));
        assert!(script_rc.contains("make fmt-check"));
        assert!(script_rc.contains("make clippy"));
        assert!(script_rc.contains("make check"));
        assert!(script_rc.contains("make test"));
        assert!(script_rc.contains("make doc"));
        assert!(script_bundle.contains("BUNDLE_MANIFEST.tsv"));
        assert!(
            roadmap.contains("## Milestone 96 — Security Closure Gate v1\n\n**Status:** complete.")
        );
        assert!(roadmap.contains(
            "## Milestone 101 — 1.0.0 Release Candidate and Final Cut\n\n**Status:** complete."
        ));
        assert!(changelog.contains("1.0.0 closure milestones (M96-M101)"));
    }

    #[test]
    fn run_with_args_rejects_too_many_arguments() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec!["examples/sample.html".to_owned(), "extra".to_owned()],
            &mut stdin,
        );
        assert_eq!(output, Err("too many arguments".to_owned()));
    }

    #[test]
    fn run_with_args_rejects_unknown_extract_format() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec!["--extract".to_owned(), "xml".to_owned(), "-".to_owned()],
            &mut stdin,
        );
        assert_eq!(output, Err("unsupported extraction format: xml".to_owned()));
    }

    #[test]
    fn run_with_args_rejects_unknown_ai_action() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec!["--ai-offline".to_owned(), "chat".to_owned(), "-".to_owned()],
            &mut stdin,
        );
        assert_eq!(output, Err("unsupported AI action: chat".to_owned()));
    }

    #[test]
    fn run_with_args_rejects_unknown_capture_option() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "capture".to_owned(),
                "--raw".to_owned(),
                "https://example.org".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );
        assert_eq!(output, Err("unsupported capture option: --raw".to_owned()));
    }

    #[test]
    fn read_input_rejects_network_url() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = read_input("https://example.com", &mut stdin);
        assert!(result.is_err());
        if let Err(error) = result {
            assert!(error.contains("network URLs must be opened"));
        }
    }

    #[test]
    fn normalize_url_input_defaults_missing_scheme_to_https() {
        assert_eq!(
            normalize_url_input("example.org/docs"),
            "https://example.org/docs"
        );
        assert_eq!(
            normalize_url_input("https://example.org/docs"),
            "https://example.org/docs"
        );
        assert_eq!(
            normalize_url_input("http://example.org/docs"),
            "http://example.org/docs"
        );
    }

    #[test]
    fn run_with_args_fetches_network_url_for_plain_output() -> Result<(), Box<dyn std::error::Error>>
    {
        let mut fetcher = MemoryFetcher::new();
        let url = IndexUrl::parse("https://example.org/docs")?;
        fetcher.insert(
            url,
            "<html><head><title>Remote</title></head><body><main><p>Fetched page.</p><a href=\"/next\">Next</a></main></body></html>",
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args_and_fetcher(
            vec!["--plain".to_owned(), "https://example.org/docs".to_owned()],
            &mut stdin,
            &fetcher,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(rendered)) = result {
            assert!(rendered.contains("# Remote"));
            assert!(rendered.contains("Fetched page."));
            assert!(rendered.contains("Next -> https://example.org/next"));
        }
        Ok(())
    }

    #[test]
    fn run_with_args_fetches_schemeless_url_as_https() -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let url = IndexUrl::parse("https://example.org/docs")?;
        fetcher.insert(
            url,
            "<html><head><title>Schemeless</title></head><body><main><p>Fetched over HTTPS.</p></main></body></html>",
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args_and_fetcher(
            vec!["--plain".to_owned(), "example.org/docs".to_owned()],
            &mut stdin,
            &fetcher,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(rendered)) = result {
            assert!(rendered.contains("# Schemeless"));
            assert!(rendered.contains("Fetched over HTTPS."));
        }
        Ok(())
    }

    #[test]
    fn run_with_args_seeds_tui_url_history_for_initial_url()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let url = IndexUrl::parse("https://example.org/docs")?;
        fetcher.insert(
            url,
            "<html><head><title>Remote</title></head><body><main><p>Fetched page.</p></main></body></html>",
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result =
            run_with_args_and_fetcher(vec!["example.org/docs".to_owned()], &mut stdin, &fetcher);

        assert!(matches!(result, Ok(CliAction::Tui { .. })));
        if let Ok(CliAction::Tui { url_history, .. }) = result {
            assert_eq!(url_history, vec!["https://example.org/docs".to_owned()]);
        }
        Ok(())
    }

    #[test]
    fn fetch_document_cached_with_log_returns_history_and_response_preview()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let url = IndexUrl::parse("https://example.org/docs")?;
        fetcher.insert(
            url,
            "<html><head><title>Remote</title></head><body><main><p>Fetched token=secret page.</p></main></body></html>",
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut cache = TransformedDocumentCache::new();
        let sequence = RefCell::new(0);

        let result =
            fetch_document_cached_with_log(&fetcher, &mut cache, &sequence, "example.org/docs")?;

        assert_eq!(result.document.title, "Remote");
        assert_eq!(
            result.visited_url.as_deref(),
            Some("https://example.org/docs")
        );
        let log = result.response_log.ok_or("missing response log")?;
        assert_eq!(log.method, "GET");
        assert!(!log.body_preview.contains("secret"));
        assert!(log.body_preview.contains("[REDACTED]"));
        Ok(())
    }

    #[test]
    fn fetch_document_cached_progress_reports_all_runtime_steps()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let url = IndexUrl::parse("https://example.org/progress")?;
        fetcher.insert(
            url,
            "<html><head><title>Progress</title></head><body><main><p>Body.</p></main></body></html>",
        );
        let mut cache = TransformedDocumentCache::new();
        let mut sequence = 0_u64;
        let mut progress = Vec::new();

        let result = fetch_document_cached_with_log_with_counter_with_progress(
            &fetcher,
            &mut cache,
            &mut sequence,
            "https://example.org/progress",
            &mut |message| progress.push(message),
        )?;

        assert_eq!(
            result.visited_url.as_deref(),
            Some("https://example.org/progress")
        );
        assert_eq!(
            progress,
            vec![
                "fetching https://example.org/progress".to_owned(),
                "parsing https://example.org/progress".to_owned(),
                "transforming https://example.org/progress".to_owned(),
                "rendering https://example.org/progress".to_owned(),
            ]
        );
        Ok(())
    }

    #[test]
    fn submit_form_cached_transforms_post_response() -> Result<(), Box<dyn std::error::Error>> {
        let form = Form {
            name: "login".to_owned(),
            method: "POST".to_owned(),
            action: "https://example.org/login".to_owned(),
            inputs: vec![Input {
                name: "user".to_owned(),
                kind: "text".to_owned(),
                value: Some("index".to_owned()),
                required: true,
            }],
            buttons: Vec::new(),
        };
        let submission = form.submit(None, &[])?;
        let mut fetcher = MemoryFetcher::new();
        fetcher.insert_form_response(
            &submission,
            Response {
                final_url: IndexUrl::parse("https://example.org/account")?,
                redirects: Vec::new(),
                mime_type: Some("text/html".to_owned()),
                body: "<html><head><title>Account</title></head><body><main><p>Submitted form.</p></main></body></html>".to_owned(),
            },
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut cache = TransformedDocumentCache::new();

        let document = submit_form_cached(&fetcher, &mut cache, &submission)?;

        assert_eq!(document.title, "Account");
        assert!(document.nodes.iter().any(
            |node| matches!(node, IndexNode::Paragraph(text) if text.contains("Submitted form"))
        ));

        let sequence = RefCell::new(0);
        let result = submit_form_cached_with_log(&fetcher, &mut cache, &sequence, &submission)?;
        assert_eq!(result.document.title, "Account");
        assert_eq!(
            result.visited_url.as_deref(),
            Some("https://example.org/account")
        );
        let log = result.response_log.ok_or("missing response log")?;
        assert_eq!(log.sequence, 1);
        assert_eq!(log.method, "POST");
        assert!(log.body_preview.contains("Submitted form"));
        Ok(())
    }

    #[test]
    fn submit_form_cached_progress_reports_all_runtime_steps()
    -> Result<(), Box<dyn std::error::Error>> {
        let form = Form {
            name: "search".to_owned(),
            method: "GET".to_owned(),
            action: "https://example.org/search".to_owned(),
            inputs: vec![Input {
                name: "q".to_owned(),
                kind: "text".to_owned(),
                value: Some("index".to_owned()),
                required: true,
            }],
            buttons: Vec::new(),
        };
        let submission = form.submit(None, &[])?;

        let mut fetcher = MemoryFetcher::new();
        fetcher.insert_response(
            submission.action.clone(),
            Response {
                final_url: IndexUrl::parse("https://example.org/search?q=index")?,
                redirects: Vec::new(),
                mime_type: Some("text/html".to_owned()),
                body: "<html><head><title>Search</title></head><body><main><p>Result.</p></main></body></html>".to_owned(),
            },
        );
        let mut cache = TransformedDocumentCache::new();
        let mut sequence = 0_u64;
        let mut progress = Vec::new();

        let result = submit_form_cached_with_log_with_counter_with_progress(
            &fetcher,
            &mut cache,
            &mut sequence,
            &submission,
            &mut |message| progress.push(message),
        )?;

        assert_eq!(
            result.visited_url.as_deref(),
            Some("https://example.org/search?q=index")
        );
        assert_eq!(
            progress,
            vec![
                "submitting GET https://example.org/search?q=index".to_owned(),
                "parsing https://example.org/search?q=index".to_owned(),
                "transforming https://example.org/search?q=index".to_owned(),
                "rendering https://example.org/search?q=index".to_owned(),
            ]
        );
        Ok(())
    }

    #[test]
    fn forum_navigation_fetch_logs_are_redacted_and_track_visited_url()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut fetcher = MemoryFetcher::new();
        let url = IndexUrl::parse("https://news.ycombinator.com/item?id=42")?;
        fetcher.insert(
            url,
            "<html><head><title>HN Item</title><link rel=\"canonical\" href=\"https://news.ycombinator.com/item?id=42\"></head><body><main><h1>Story</h1><p>token=verysecret</p><a href=\"https://example.org/story\">Story</a></main></body></html>",
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut cache = TransformedDocumentCache::new();
        let sequence = RefCell::new(0);
        let result = fetch_document_cached_with_log(
            &fetcher,
            &mut cache,
            &sequence,
            "news.ycombinator.com/item?id=42",
        )?;

        assert!(result.document.title.starts_with("Hacker News:"));
        assert_eq!(
            result.visited_url.as_deref(),
            Some("https://news.ycombinator.com/item?id=42")
        );
        let log = result.response_log.ok_or("missing response log")?;
        assert_eq!(log.method, "GET");
        assert!(!log.body_preview.contains("verysecret"));
        assert!(log.body_preview.contains("[REDACTED]"));
        Ok(())
    }

    #[test]
    fn forum_form_submission_maps_and_logs_redacted_preview()
    -> Result<(), Box<dyn std::error::Error>> {
        let fixture = fs::read_to_string(workspace_path(
            "crates/index-transformer/tests/fixtures/adapters/legacy-forum-thread.html",
        ))?;
        let document = super::transform_html(fixture);
        let form = document
            .nodes
            .iter()
            .find_map(|node| match node {
                IndexNode::Form(form) if form.name == "legacy-search" => Some(form.clone()),
                _ => None,
            })
            .ok_or("missing legacy-search form")?;
        let submission = form.submit(
            Some(&IndexUrl::parse("https://forums.tomshardware.com/")?),
            &[("q", "password=super-secret")],
        )?;

        let mut fetcher = MemoryFetcher::new();
        let requested = submission.action.clone();
        fetcher.insert_response(
            requested.clone(),
            Response {
                final_url: requested,
                redirects: Vec::new(),
                mime_type: Some("text/html".to_owned()),
                body: "<html><head><title>Forum Search</title></head><body><main><p>results auth=secret-token</p></main></body></html>".to_owned(),
            },
        );
        let fetcher = SecureFetcher::new(fetcher);
        let mut cache = TransformedDocumentCache::new();
        let sequence = RefCell::new(0);
        let result = submit_form_cached_with_log(&fetcher, &mut cache, &sequence, &submission)?;

        assert!(result.document.title.contains("Forum Search"));
        let log = result.response_log.ok_or("missing response log")?;
        assert_eq!(log.method, "GET");
        assert!(!log.body_preview.contains("secret-token"));
        assert!(log.body_preview.contains("[REDACTED]"));
        Ok(())
    }

    #[test]
    fn run_with_args_returns_diagnostic_document_for_network_failure()
    -> Result<(), Box<dyn std::error::Error>> {
        let fetcher = SecureFetcher::new(MemoryFetcher::new());
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args_and_fetcher(
            vec![
                "--plain".to_owned(),
                "https://missing.example/page".to_owned(),
            ],
            &mut stdin,
            &fetcher,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(rendered)) = result {
            assert!(rendered.contains("# Network fetch failed"));
            assert!(rendered.contains("could not fetch https://missing.example/page"));
            assert!(rendered.contains("retry the request"));
            assert!(rendered.contains("capture a local redacted fixture"));
        }
        Ok(())
    }

    #[test]
    fn run_with_args_reports_network_failure_matrix() -> Result<(), Box<dyn std::error::Error>> {
        let cases = [
            (
                "https://net.example/dns",
                FetchError::Network("dns lookup failed".to_owned()),
                "dns lookup failed",
            ),
            (
                "https://net.example/tls",
                FetchError::Network("tls handshake failed".to_owned()),
                "tls handshake failed",
            ),
            (
                "https://net.example/timeout",
                FetchError::Timeout { timeout_ms: 1000 },
                "timed out after 1000ms",
            ),
            (
                "https://net.example/status",
                FetchError::HttpStatus {
                    status: 503,
                    url: "https://net.example/status".to_owned(),
                },
                "HTTP status 503",
            ),
            (
                "https://net.example/image",
                FetchError::UnsupportedContentType("image/png".to_owned()),
                "unsupported response content type: image/png",
            ),
        ];

        for (url, error, expected) in cases {
            let parsed = IndexUrl::parse(url)?;
            let mut fetcher = MemoryFetcher::new();
            fetcher.insert_error(parsed, error);
            let fetcher = SecureFetcher::new(fetcher);
            let mut stdin = Cursor::new(Vec::<u8>::new());
            let result = run_with_args_and_fetcher(
                vec!["--plain".to_owned(), url.to_owned()],
                &mut stdin,
                &fetcher,
            );

            assert!(matches!(result, Ok(CliAction::Print(_))));
            if let Ok(CliAction::Print(rendered)) = result {
                assert!(rendered.contains("# Network fetch failed"));
                assert!(rendered.contains(expected));
                assert!(rendered.contains("bounded retry policy"));
            }
        }
        Ok(())
    }

    #[test]
    fn run_with_args_prefers_existing_local_file_over_schemeless_url()
    -> Result<(), Box<dyn std::error::Error>> {
        let path = unique_temp_file("local-domain-like-name");
        fs::write(
            &path,
            "<html><title>Local Dot Path</title><p>From file.</p></html>",
        )?;
        let fetcher = SecureFetcher::new(MemoryFetcher::new());
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args_and_fetcher(
            vec![
                "--plain".to_owned(),
                path.to_str().unwrap_or_default().to_owned(),
            ],
            &mut stdin,
            &fetcher,
        );
        fs::remove_file(&path)?;

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(rendered)) = result {
            assert!(rendered.contains("# Local Dot Path"));
            assert!(rendered.contains("From file."));
        }
        Ok(())
    }

    #[test]
    fn html_with_base_preserves_existing_base() -> Result<(), Box<dyn std::error::Error>> {
        let url = IndexUrl::parse("https://example.org/docs")?;
        let html = "<html><head><base href=\"https://docs.example/\"></head></html>";
        assert_eq!(html_with_base(&url, html), html);
        Ok(())
    }

    #[test]
    fn read_input_reads_from_stdin() {
        let mut stdin = Cursor::new(b"<title>stdin</title>".to_vec());
        let result = read_input("-", &mut stdin);
        assert_eq!(result, Ok("<title>stdin</title>".to_owned()));
    }

    #[test]
    fn read_input_reads_local_file() -> Result<(), Box<dyn std::error::Error>> {
        let path = unique_temp_file("read-input");
        fs::write(&path, "<title>file</title>")?;
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = read_input(path.to_str().unwrap_or_default(), &mut stdin);
        fs::remove_file(&path)?;
        assert_eq!(result, Ok("<title>file</title>".to_owned()));
        Ok(())
    }

    #[test]
    fn read_input_rejects_large_stdin() {
        let mut stdin = Cursor::new(b"12345".to_vec());
        let result = read_input_with_limits("-", &mut stdin, ContentLimits::new(4, 100, 20, 10));

        assert!(matches!(result, Err(error) if error.contains("content size limit exceeded")));
    }

    #[test]
    fn run_with_args_transforms_stdin_input() {
        let mut stdin = Cursor::new(
            b"<html><title>CLI</title><p>From stdin.</p><a href=\"https://example.com\">Go</a></html>"
                .to_vec(),
        );
        let result = run_with_args(vec!["--plain".to_owned(), "-".to_owned()], &mut stdin);
        assert!(result.is_ok());
        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(rendered)) = result {
            assert!(rendered.contains("# CLI"));
            assert!(rendered.contains("From stdin."));
            assert!(rendered.contains("Go -> https://example.com"));
        }
    }

    #[test]
    fn run_with_args_transforms_file_input() -> Result<(), Box<dyn std::error::Error>> {
        let path = unique_temp_file("run-with-args");
        fs::write(
            &path,
            "<html><title>CLI File</title><p>From file.</p><a href=\"https://example.com/docs\">Docs</a></html>",
        )?;
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args(
            vec![
                "--plain".to_owned(),
                path.to_str().unwrap_or_default().to_owned(),
            ],
            &mut stdin,
        );
        fs::remove_file(&path)?;
        assert!(result.is_ok());
        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(rendered)) = result {
            assert!(rendered.contains("# CLI File"));
            assert!(rendered.contains("From file."));
            assert!(rendered.contains("Docs -> https://example.com/docs"));
        }
        Ok(())
    }

    #[test]
    fn run_with_args_extracts_markdown_from_stdin() {
        let mut stdin = Cursor::new(
            b"<html><title>CLI</title><h2>Section</h2><p>From stdin.</p></html>".to_vec(),
        );
        let result = run_with_args(
            vec![
                "--extract".to_owned(),
                "markdown".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(markdown)) = result {
            assert!(markdown.contains("# CLI"));
            assert!(markdown.contains("## Section"));
            assert!(markdown.contains("From stdin."));
        }
    }

    #[test]
    fn run_with_args_extracts_links_from_stdin() {
        let mut stdin =
            Cursor::new(b"<html><title>CLI</title><a href=\"/docs\">Docs</a></html>".to_vec());
        let result = run_with_args(
            vec!["--extract".to_owned(), "links".to_owned(), "-".to_owned()],
            &mut stdin,
        );

        assert_eq!(result, Ok(CliAction::Print("1\tDocs\t/docs\n".to_owned())));
    }

    #[test]
    fn run_with_args_extracts_json_from_stdin() {
        let mut stdin = Cursor::new(b"<html><title>CLI</title><p>From stdin.</p></html>".to_vec());
        let result = run_with_args(
            vec!["--extract".to_owned(), "json".to_owned(), "-".to_owned()],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(json)) = result {
            assert!(json.contains("\"title\": \"CLI\""));
            assert!(json.contains("\"type\": \"paragraph\""));
        }
    }

    #[test]
    fn run_with_args_runs_offline_ai_from_stdin() {
        let mut stdin = Cursor::new(
            b"<html><title>CLI AI</title><p>token=secret Authorization: Bearer abc123</p></html>"
                .to_vec(),
        );
        let result = run_with_args(
            vec![
                "--ai-offline".to_owned(),
                "summarize".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(output)) = result {
            assert!(output.contains("Offline summary"));
            assert!(output.contains("[REDACTED]"));
            assert!(!output.contains("abc123"));
        }
    }

    #[test]
    fn run_with_args_captures_redacted_artifact_from_stdin() {
        let mut stdin = Cursor::new(
            b"<html><form><input name=\"password\" value=\"secret\"></form><p>public</p></html>"
                .to_vec(),
        );
        let result = run_with_args(
            vec![
                "capture".to_owned(),
                "--redact".to_owned(),
                "https://example.org/login?token=url-secret".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(output)) = result {
            assert!(output.contains("index-capture-v1"));
            assert!(output.contains("public"));
            assert!(output.contains("[REDACTED]"));
            assert!(!output.contains("secret"));
            assert!(!output.contains("url-secret"));
        }
    }

    #[test]
    fn run_with_args_previews_capture_from_stdin() {
        let mut stdin = Cursor::new(
            b"<html><input name=\"password\" value=\"secret\"><p>public</p></html>".to_vec(),
        );
        let result = run_with_args(
            vec![
                "capture".to_owned(),
                "--preview".to_owned(),
                "--redact".to_owned(),
                "https://example.org/login?token=url-secret".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(output)) = result {
            assert!(output.contains("index-capture-preview-v1"));
            assert!(output.contains("redaction-summary-v1"));
            assert!(output.contains("fixture-submission-checklist-v1"));
            assert!(!output.contains("url-secret"));
            assert!(!output.contains("secret"));
        }
    }

    #[test]
    fn run_with_args_validates_capture_artifact_from_stdin()
    -> Result<(), Box<dyn std::error::Error>> {
        let artifact = index_capture::capture_redacted(&index_capture::CaptureRequest::new(
            "https://example.org/page?token=secret",
            "<main>Public</main>",
        )?)
        .to_text();
        let mut stdin = Cursor::new(artifact.into_bytes());
        let result = run_with_args(
            vec![
                "capture".to_owned(),
                "--validate".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(output)) = result {
            assert!(output.contains("index-capture-validation-v1"));
            assert!(output.contains("status: ok"));
        }
        Ok(())
    }

    #[test]
    fn run_with_args_emits_capture_catalog_entry() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(
            vec![
                "capture".to_owned(),
                "--catalog-entry".to_owned(),
                workspace_path("examples/sample.html"),
            ],
            &mut stdin,
        );

        assert!(
            matches!(output, Ok(CliAction::Print(entry)) if entry.contains("examples/sample.html") && entry.contains("Tier 0"))
        );
    }

    #[test]
    fn run_with_args_captures_schemeless_source_url_as_https() {
        let mut stdin = Cursor::new(b"<html><p>public</p></html>".to_vec());
        let result = run_with_args(
            vec![
                "capture".to_owned(),
                "--redact".to_owned(),
                "example.org/login".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(output)) = result {
            assert!(output.contains("source_url: https://example.org/login"));
            assert!(output.contains("index capture --redact https://example.org/login"));
        }
    }

    #[test]
    fn run_with_args_rejects_unsafe_capture_source_url() {
        let mut stdin = Cursor::new(b"<main>Bad</main>".to_vec());
        let result = run_with_args(
            vec![
                "capture".to_owned(),
                "--redact".to_owned(),
                "javascript:alert(1)".to_owned(),
                "-".to_owned(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Err(error) if error.contains("scheme is not allowed")));
    }

    #[test]
    fn run_with_args_defaults_to_tui_action() {
        let mut stdin = Cursor::new(b"<title>TUI</title><p>Interactive.</p>".to_vec());
        let result = run_with_args(vec!["-".to_owned()], &mut stdin);
        assert!(matches!(result, Ok(CliAction::Tui { .. })));
    }

    #[test]
    fn run_with_args_accepts_initial_reader_profile() {
        let mut stdin = Cursor::new(b"<title>TUI</title><p>Interactive.</p>".to_vec());
        let result = run_with_args(
            vec!["--profile".to_owned(), "docs".to_owned(), "-".to_owned()],
            &mut stdin,
        );

        assert!(matches!(
            result,
            Ok(CliAction::Tui {
                profile: ReaderProfile::Docs,
                ..
            })
        ));
    }

    #[test]
    fn run_with_args_rejects_unknown_reader_profile() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args(vec!["--profile".to_owned(), "loud".to_owned()], &mut stdin);

        assert!(matches!(result, Err(error) if error.contains("unsupported reader profile")));
    }

    #[test]
    fn run_with_args_help_paths_cover_missing_arguments() {
        let help_cases: Vec<Vec<&str>> = vec![
            vec!["--profile"],
            vec!["--help"],
            vec!["-h"],
            vec!["doctor", "extra"],
            vec!["quickstart", "extra"],
            vec!["--benchmark"],
            vec!["--save"],
            vec!["--save", "markdown"],
            vec!["--save", "markdown", "-"],
            vec!["--citations"],
            vec!["--section"],
            vec!["--section", "Intro"],
            vec!["--batch-extract"],
            vec!["--batch-extract", "markdown"],
            vec!["--plain"],
            vec!["--extract"],
            vec!["--extract", "markdown"],
            vec!["--ai-offline"],
            vec!["--ai-offline", "summarize"],
        ];

        for case in help_cases {
            let mut stdin = Cursor::new(Vec::<u8>::new());
            let args = case
                .iter()
                .map(|value| (*value).to_owned())
                .collect::<Vec<_>>();
            let result = run_with_args(args, &mut stdin);
            assert!(
                matches!(result, Ok(CliAction::Help(_))),
                "expected help for case {case:?}, got {result:?}"
            );
        }

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let profile_only =
            run_with_args(vec!["--profile".to_owned(), "docs".to_owned()], &mut stdin);
        assert!(matches!(
            profile_only,
            Ok(CliAction::Tui {
                profile: ReaderProfile::Docs,
                ..
            })
        ));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let too_many = run_with_args(
            vec!["--benchmark".to_owned(), "-".to_owned(), "extra".to_owned()],
            &mut stdin,
        );
        assert_eq!(too_many, Err("too many arguments".to_owned()));
    }

    #[test]
    fn run_with_args_returns_quickstart_report() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let output = run_with_args(vec!["quickstart".to_owned()], &mut stdin);

        assert!(
            matches!(output, Ok(CliAction::Print(_))),
            "expected quickstart report, got {output:?}"
        );
        if let Ok(CliAction::Print(report)) = output {
            assert!(report.contains("index-quickstart-v1"));
            assert!(report.contains("step_1_open_url"));
            assert!(report.contains("step_4_use_forms"));
        }
    }

    #[test]
    fn run_with_args_capture_and_adapter_error_paths_are_actionable() {
        let help_cases: Vec<Vec<&str>> = vec![
            vec!["capture"],
            vec!["capture", "--validate"],
            vec!["capture", "--catalog-entry"],
            vec!["capture", "--preview"],
            vec!["capture", "--redact"],
            vec!["adapter"],
            vec!["adapter", "check"],
            vec!["adapter", "scaffold"],
            vec!["adapter", "diff"],
            vec!["adapter", "diff", "fixture-only"],
            vec!["artifact"],
            vec!["artifact", "inspect"],
        ];
        for case in help_cases {
            let mut stdin = Cursor::new(Vec::<u8>::new());
            let args = case
                .iter()
                .map(|value| (*value).to_owned())
                .collect::<Vec<_>>();
            let result = run_with_args(args, &mut stdin);
            assert!(
                matches!(result, Ok(CliAction::Help(_))),
                "expected help for case {case:?}, got {result:?}"
            );
        }

        let err_cases: Vec<(Vec<&str>, &str)> = vec![
            (
                vec!["capture", "--validate", "-", "extra"],
                "too many arguments",
            ),
            (
                vec![
                    "capture",
                    "--catalog-entry",
                    "examples/sample.html",
                    "extra",
                ],
                "too many arguments",
            ),
            (
                vec!["capture", "--preview", "--oops", "example.org", "-"],
                "unsupported capture option: --oops",
            ),
            (
                vec!["capture", "--redact", "example.org", "-", "extra"],
                "too many arguments",
            ),
            (
                vec!["adapter", "check", "fixture.html", "extra"],
                "too many arguments",
            ),
            (
                vec!["adapter", "scaffold", "fixture.html", "extra"],
                "too many arguments",
            ),
            (
                vec!["adapter", "diff", "fixture.html", "manifest.txt", "extra"],
                "too many arguments",
            ),
            (
                vec!["artifact", "inspect", "example.org/docs", "extra"],
                "too many arguments",
            ),
            (
                vec!["artifact", "unknown"],
                "unsupported artifact command: unknown",
            ),
        ];
        for (case, expected) in err_cases {
            let mut stdin = Cursor::new(Vec::<u8>::new());
            let args = case
                .iter()
                .map(|value| (*value).to_owned())
                .collect::<Vec<_>>();
            let result = run_with_args(args, &mut stdin);
            assert_eq!(result, Err(expected.to_owned()));
        }
    }

    #[test]
    fn run_with_args_artifact_inspect_reports_context_matrix() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args(
            vec![
                "artifact".to_owned(),
                "inspect".to_owned(),
                "example.org/docs".to_owned(),
            ],
            &mut stdin,
        );
        assert!(matches!(result, Ok(CliAction::Print(text))
                if text.contains("index-artifact-inspect-v1")
                && text.contains("target: https://example.org/docs")
                && text.contains("context: live-get")
                && text.contains("context: live-submit")
                && text.contains("context: offline")));
    }

    #[test]
    fn parse_adapter_manifest_and_tier_mapping_cover_error_paths() {
        let missing_header = parse_adapter_manifest("adapter: docs");
        assert_eq!(
            missing_header,
            Err("adapter manifest missing header".to_owned())
        );

        let invalid_number = parse_adapter_manifest(
            "index-adapter-manifest-v1\nadapter: docs\nsupport_tier: nope\nnodes: 1\nlinks: 1\nforms: 0\ntables: 0\nregions: 0\n",
        );
        assert!(matches!(invalid_number, Err(error) if error.contains("invalid support_tier")));

        let missing_adapter = parse_adapter_manifest(
            "index-adapter-manifest-v1\nsupport_tier: 2\nnodes: 1\nlinks: 1\nforms: 0\ntables: 0\nregions: 0\n",
        );
        assert_eq!(
            missing_adapter,
            Err("adapter manifest missing adapter".to_owned())
        );

        let manifest = parse_adapter_manifest(
            "index-adapter-manifest-v1\nadapter: top100.baseline\nsupport_tier: 2\nnodes: 7\nlinks: 3\nforms: 1\ntables: 1\nregions: 2\nmarkdown_contains: # Heading\nignored: value\n",
        );
        assert_eq!(
            manifest,
            Ok(AdapterManifest {
                adapter: "top100.baseline".to_owned(),
                support_tier: 2,
                nodes: 7,
                links: 3,
                forms: 1,
                tables: 1,
                regions: 2,
                markdown_contains: vec!["# Heading".to_owned()],
            })
        );

        assert_eq!(adapter_support_tier("adapter"), 3);
        assert_eq!(adapter_support_tier("strong-generic"), 2);
        assert_eq!(adapter_support_tier("partial-generic"), 1);
        assert_eq!(adapter_support_tier("fallback"), 1);
        assert_eq!(adapter_support_tier("failed"), 0);
        assert_eq!(adapter_support_tier("unknown"), 0);
    }

    #[test]
    fn shelf_search_arg_parsing_and_formatting_cover_json_and_markdown_paths() {
        let missing_format = parse_shelf_search_args(vec!["--format"].into_iter());
        assert_eq!(
            missing_format,
            Err("missing shelf search format".to_owned())
        );

        let unsupported_format =
            parse_shelf_search_args(vec!["--format", "xml", "query"].into_iter());
        assert_eq!(
            unsupported_format,
            Err("unsupported shelf search format: xml".to_owned())
        );

        let parsed = parse_shelf_search_args(
            vec!["--format", "markdown", "typed", "query", "terms"].into_iter(),
        );
        assert_eq!(
            parsed,
            Ok((ShelfSearchFormat::Markdown, "typed query terms".to_owned()))
        );

        let results = vec![ShelfSearchResult {
            id: "rec-1".to_owned(),
            title: "Line \"One\"\nTabbed\tValue".to_owned(),
            source_url: None,
            score: 88,
            matched_fields: vec!["title".to_owned(), "markdown".to_owned()],
        }];

        let markdown = format_shelf_search("query", ShelfSearchFormat::Markdown, &results);
        assert!(markdown.contains("- Line \"One\""));
        assert!(markdown.contains("`rec-1` score 88 fields title,markdown"));

        let json = format_shelf_search("query", ShelfSearchFormat::Json, &results);
        assert!(json.contains("\"source_url\":null"));
        assert!(json.contains("\\\"One\\\""));
        assert!(json.contains("\\n"));
        assert!(json.contains("\\t"));
        assert!(json.contains("\"matched_fields\":[\"title\",\"markdown\"]"));
    }

    #[test]
    fn load_offline_document_supports_capture_artifacts() -> Result<(), Box<dyn std::error::Error>>
    {
        let capture = index_capture::capture_redacted(&index_capture::CaptureRequest::new(
            "https://example.org/articles/1",
            "<html><head><title>Captured</title></head><body><main><p>Body</p><a href=\"/next\">Next</a></main></body></html>",
        )?)
        .to_text();
        let path = unique_temp_file("capture-artifact");
        fs::write(&path, capture)?;

        let document = load_offline_document(path.to_str().unwrap_or_default())?;
        fs::remove_file(&path)?;

        assert_eq!(document.title, "Captured");
        let rendered =
            index_renderer::render_document(&document, index_renderer::RenderOptions::default());
        assert!(rendered.contains("Body"));
        assert!(rendered.contains("Next -> https://example.org/next"));
        Ok(())
    }

    #[test]
    fn document_counts_and_path_health_cover_section_recursion_and_existing_dirs() {
        let mut nested = IndexDocument::titled("Nested");
        nested.push(IndexNode::Section {
            role: index_core::SectionRole::Main,
            title: Some("Main".to_owned()),
            collapsed: false,
            nodes: vec![
                IndexNode::Paragraph("Body".to_owned()),
                IndexNode::Link(Link::new("Docs", "https://example.org/docs")),
            ],
        });
        nested.push(IndexNode::Link(Link::new("Root", "https://example.org")));

        assert_eq!(document_node_count(&nested), 4);
        assert_eq!(document_link_count(&nested), 2);

        let existing_dir = std::env::temp_dir();
        let ok = path_health("temp", existing_dir.to_str().unwrap_or_default());
        assert_eq!(ok, "temp: ok");
    }

    #[test]
    fn run_with_args_saves_markdown_export() -> Result<(), Box<dyn std::error::Error>> {
        let input = unique_temp_file("save-input");
        let output = unique_temp_file("save-output");
        fs::write(&input, "<title>Save</title><main><p>Body</p></main>")?;
        let mut stdin = Cursor::new(Vec::<u8>::new());

        let result = run_with_args(
            vec![
                "--save".to_owned(),
                "markdown".to_owned(),
                input.display().to_string(),
                output.display().to_string(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        assert!(fs::read_to_string(&output)?.contains("Body"));
        fs::remove_file(&input)?;
        fs::remove_file(&output)?;
        Ok(())
    }

    #[test]
    fn shelf_list_and_show_outputs_are_stable() {
        let mut record = ShelfRecord::new(
            "Shelf Page",
            Some("https://example.org/shelf".to_owned()),
            Some("strong-generic".to_owned()),
            "12345",
            vec!["https://example.org/ref".to_owned()],
            "/tmp/shelf.md",
            "/tmp/shelf.json",
        );
        record.add_tag("docs");
        record.set_note("Read again");
        let mut shelf = KnowledgeShelf::new();
        shelf.upsert(record.clone());

        let list = format_shelf_list(&shelf);
        assert!(list.contains("index-shelf-list-v1"));
        assert!(list.contains("Shelf Page"));
        assert!(list.contains("strong-generic"));

        let shown = format_shelf_record(&record);
        assert!(shown.contains("index-shelf-record-v1"));
        assert!(shown.contains("tags: docs"));
        assert!(shown.contains("note: Read again"));
    }

    #[test]
    fn shelf_commands_save_list_show_tag_and_note() -> Result<(), Box<dyn std::error::Error>> {
        let root = unique_temp_file("shelf-root");
        let shelf_dir = root.with_extension("dir");
        let paths = ShelfPaths {
            shelf_dir: shelf_dir.clone(),
            exports_dir: shelf_dir.join("exports"),
            index_path: shelf_dir.join("index-shelf.txt"),
        };
        let fetcher = SecureFetcher::new(MemoryFetcher::new());
        let mut stdin = Cursor::new(
            "<title>Shelf</title><main><p>Body</p><a href=\"https://example.org/ref\">Ref</a></main>",
        );
        let saved = run_shelf_command_with_paths(
            vec!["save".to_owned(), "-".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        let CliAction::Print(saved) = saved else {
            return Err("expected print action".into());
        };
        assert!(saved.starts_with("shelf-saved\t"));
        let id = saved
            .split('\t')
            .nth(1)
            .ok_or("missing shelf id")?
            .to_owned();

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let listed = run_shelf_command_with_paths(
            vec!["list".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(listed, CliAction::Print(output) if output.contains(&id)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        run_shelf_command_with_paths(
            vec!["tag".to_owned(), id.clone(), "docs".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        let mut stdin = Cursor::new(Vec::<u8>::new());
        run_shelf_command_with_paths(
            vec![
                "note".to_owned(),
                id.clone(),
                "read".to_owned(),
                "again".to_owned(),
            ]
            .into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let shown = run_shelf_command_with_paths(
            vec!["show".to_owned(), id].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        let CliAction::Print(shown) = shown else {
            return Err("expected show output".into());
        };
        assert!(shown.contains("tags: docs"));
        assert!(shown.contains("note: read again"));
        assert!(shown.contains("citations:"));
        if shelf_dir.exists() {
            std::fs::remove_dir_all(&shelf_dir)?;
        }
        Ok(())
    }

    #[test]
    fn shelf_commands_report_help_and_missing_records() -> Result<(), Box<dyn std::error::Error>> {
        let root = unique_temp_file("shelf-errors");
        let shelf_dir = root.with_extension("dir");
        let paths = ShelfPaths {
            shelf_dir: shelf_dir.clone(),
            exports_dir: shelf_dir.join("exports"),
            index_path: shelf_dir.join("index-shelf.txt"),
        };
        let fetcher = SecureFetcher::new(MemoryFetcher::new());
        let mut stdin = Cursor::new(Vec::<u8>::new());

        let help = run_shelf_command_with_paths(
            Vec::<String>::new().into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(help, CliAction::Help(_)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let list = run_shelf_command_with_paths(
            vec!["list".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(list, CliAction::Print(output) if output == "index-shelf-list-v1"));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let show = run_shelf_command_with_paths(
            vec!["show".to_owned(), "missing".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(matches!(show, Err(error) if error.contains("not found")));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let tag_help = run_shelf_command_with_paths(
            vec!["tag".to_owned(), "missing".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(tag_help, CliAction::Help(_)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let note_help = run_shelf_command_with_paths(
            vec!["note".to_owned(), "missing".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(note_help, CliAction::Help(_)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let unsupported = run_shelf_command_with_paths(
            vec!["remove".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(matches!(unsupported, Err(error) if error.contains("unsupported shelf command")));
        if shelf_dir.exists() {
            std::fs::remove_dir_all(&shelf_dir)?;
        }
        Ok(())
    }

    #[test]
    fn shelf_commands_reject_invalid_arities() -> Result<(), Box<dyn std::error::Error>> {
        let root = unique_temp_file("shelf-arities");
        let shelf_dir = root.with_extension("dir");
        let paths = ShelfPaths {
            shelf_dir: shelf_dir.clone(),
            exports_dir: shelf_dir.join("exports"),
            index_path: shelf_dir.join("index-shelf.txt"),
        };
        let fetcher = SecureFetcher::new(MemoryFetcher::new());
        let mut stdin = Cursor::new(Vec::<u8>::new());

        let save_help = run_shelf_command_with_paths(
            vec!["save".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(save_help, CliAction::Help(_)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let save_too_many = run_shelf_command_with_paths(
            vec!["save".to_owned(), "-".to_owned(), "extra".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(matches!(save_too_many, Err(error) if error == "too many arguments"));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let list_too_many = run_shelf_command_with_paths(
            vec!["list".to_owned(), "extra".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(matches!(list_too_many, Err(error) if error == "too many arguments"));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let show_help = run_shelf_command_with_paths(
            vec!["show".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(show_help, CliAction::Help(_)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let show_too_many = run_shelf_command_with_paths(
            vec!["show".to_owned(), "one".to_owned(), "two".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(matches!(show_too_many, Err(error) if error == "too many arguments"));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let tag_help = run_shelf_command_with_paths(
            vec!["tag".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(tag_help, CliAction::Help(_)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let tag_too_many = run_shelf_command_with_paths(
            vec![
                "tag".to_owned(),
                "one".to_owned(),
                "two".to_owned(),
                "three".to_owned(),
            ]
            .into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(matches!(tag_too_many, Err(error) if error == "too many arguments"));

        if shelf_dir.exists() {
            std::fs::remove_dir_all(&shelf_dir)?;
        }
        Ok(())
    }

    #[test]
    fn shelf_search_finds_local_exports_without_network() -> Result<(), Box<dyn std::error::Error>>
    {
        let root = unique_temp_file("shelf-search");
        let shelf_dir = root.with_extension("dir");
        let paths = ShelfPaths {
            shelf_dir: shelf_dir.clone(),
            exports_dir: shelf_dir.join("exports"),
            index_path: shelf_dir.join("index-shelf.txt"),
        };
        fs::create_dir_all(&paths.exports_dir)?;

        let mut record = ShelfRecord::new(
            "Offline Rust Notes",
            Some("https://example.org/rust".to_owned()),
            Some("strong-generic".to_owned()),
            "123",
            vec!["https://example.org/citation".to_owned()],
            paths.exports_dir.join("rust.md").display().to_string(),
            paths.exports_dir.join("rust.json").display().to_string(),
        );
        record.add_tag("docs");
        record.set_note("ownership");
        fs::write(&record.markdown_path, "# Borrowing\nLocal export body")?;
        let mut shelf = KnowledgeShelf::new();
        shelf.upsert(record.clone());
        shelf.save_to_path(&paths.index_path)?;

        let fetcher = SecureFetcher::new(MemoryFetcher::new());
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let links = run_shelf_command_with_paths(
            vec!["search".to_owned(), "borrowing".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(links, CliAction::Print(_)));
        if let CliAction::Print(output) = links {
            assert!(output.contains("index-shelf-search-v1"));
            assert!(output.contains(&record.id));
            assert!(output.contains("markdown_headings"));
        }

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let markdown = run_shelf_command_with_paths(
            vec![
                "search".to_owned(),
                "--format".to_owned(),
                "markdown".to_owned(),
                "ownership".to_owned(),
            ]
            .into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(
            matches!(markdown, CliAction::Print(output) if output.contains("# Shelf search: ownership") && output.contains("Offline Rust Notes"))
        );

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let json = run_shelf_command_with_paths(
            vec![
                "search".to_owned(),
                "--format".to_owned(),
                "json".to_owned(),
                "docs".to_owned(),
            ]
            .into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(
            matches!(json, CliAction::Print(output) if output.contains("\"query\":\"docs\"") && output.contains("\"matched_fields\":[\"tags\""))
        );

        if shelf_dir.exists() {
            std::fs::remove_dir_all(&shelf_dir)?;
        }
        Ok(())
    }

    #[test]
    fn shelf_search_reports_help_and_format_errors() -> Result<(), Box<dyn std::error::Error>> {
        let root = unique_temp_file("shelf-search-errors");
        let shelf_dir = root.with_extension("dir");
        let paths = ShelfPaths {
            shelf_dir: shelf_dir.clone(),
            exports_dir: shelf_dir.join("exports"),
            index_path: shelf_dir.join("index-shelf.txt"),
        };
        let fetcher = SecureFetcher::new(MemoryFetcher::new());

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let help = run_shelf_command_with_paths(
            vec!["search".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        )?;
        assert!(matches!(help, CliAction::Help(_)));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let missing_format = run_shelf_command_with_paths(
            vec!["search".to_owned(), "--format".to_owned()].into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(matches!(missing_format, Err(error) if error == "missing shelf search format"));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let bad_format = run_shelf_command_with_paths(
            vec![
                "search".to_owned(),
                "--format".to_owned(),
                "xml".to_owned(),
                "query".to_owned(),
            ]
            .into_iter(),
            &mut stdin,
            &fetcher,
            &paths,
        );
        assert!(
            matches!(bad_format, Err(error) if error.contains("unsupported shelf search format"))
        );

        if shelf_dir.exists() {
            std::fs::remove_dir_all(&shelf_dir)?;
        }
        Ok(())
    }

    #[test]
    fn run_with_args_extracts_citations() {
        let mut stdin = Cursor::new(
            b"<title>Citations</title><main><a href=\"https://example.org/ref\">Ref</a><a href=\"/local\">Local</a></main>"
                .to_vec(),
        );

        let result = run_with_args(vec!["--citations".to_owned(), "-".to_owned()], &mut stdin);

        assert_eq!(
            result,
            Ok(CliAction::Print(
                "1\tRef\thttps://example.org/ref\n".to_owned()
            ))
        );
    }

    #[test]
    fn run_with_args_exports_selected_section() {
        let mut stdin = Cursor::new(
            b"<title>Sections</title><main><h2>Keep</h2><p>Yes</p><h2>Skip</h2><p>No</p></main>"
                .to_vec(),
        );

        let result = run_with_args(
            vec!["--section".to_owned(), "Keep".to_owned(), "-".to_owned()],
            &mut stdin,
        );

        assert_eq!(result, Ok(CliAction::Print("## Keep\n\nYes\n".to_owned())));
    }

    #[test]
    fn run_with_args_batch_extracts_local_files_and_capture_artifacts()
    -> Result<(), Box<dyn std::error::Error>> {
        let html_path = unique_temp_file("batch-html");
        let capture_path = unique_temp_file("batch-capture");
        fs::write(&html_path, "<title>One</title><main><p>First</p></main>")?;
        fs::write(
            &capture_path,
            "index-capture-v1\nsource_url: https://example.org/captured\nreproduce: index capture --redact https://example.org/captured fixture.html\n--- redacted_html ---\n<title>Two</title><main><p>Second</p></main>\n--- diagnostics ---\n",
        )?;
        let mut stdin = Cursor::new(Vec::<u8>::new());

        let result = run_with_args(
            vec![
                "--batch-extract".to_owned(),
                "markdown".to_owned(),
                html_path.display().to_string(),
                capture_path.display().to_string(),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(output)) = result {
            assert!(output.contains("==> "));
            assert!(output.contains("First"));
            assert!(output.contains("Second"));
        }
        fs::remove_file(&html_path)?;
        fs::remove_file(&capture_path)?;
        Ok(())
    }

    #[test]
    fn adapter_check_reports_adapter_fixture() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args(
            vec![
                "adapter".to_owned(),
                "check".to_owned(),
                workspace_path(
                    "crates/index-transformer/tests/fixtures/adapters/gitlab-project.html",
                ),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(report)) = result {
            assert!(report.contains("index-adapter-check-v1"));
            assert!(report.contains("adapter: gitlab"));
            assert!(report.contains("support_tier: 3"));
            assert!(report.contains("fallback_reason: adapter emitted task view"));
            assert!(report.contains("checklist: docs/FIXTURE_INTAKE.md"));
            assert!(report.contains("--- markdown ---"));
        }
    }

    #[test]
    fn adapter_check_report_matches_golden_snapshot() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args(
            vec![
                "adapter".to_owned(),
                "check".to_owned(),
                workspace_path(
                    "crates/index-transformer/tests/fixtures/adapters/mdn-reference.html",
                ),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(report)) = result {
            let report = report.replace(
                &workspace_path(
                    "crates/index-transformer/tests/fixtures/adapters/mdn-reference.html",
                ),
                "crates/index-transformer/tests/fixtures/adapters/mdn-reference.html",
            );
            assert_eq!(
                report.trim_end(),
                include_str!("../tests/golden/adapter-mdn-report.txt").trim_end()
            );
        }
    }

    #[test]
    fn shelf_search_links_match_golden_snapshot() {
        let mut record = ShelfRecord::new(
            "Offline Rust Notes",
            Some("https://example.org/rust".to_owned()),
            Some("strong-generic".to_owned()),
            "123",
            vec!["https://example.org/citation".to_owned()],
            "/tmp/index-shelf/rust.md",
            "/tmp/index-shelf/rust.json",
        );
        record.add_tag("docs");
        record.set_note("ownership");
        let mut shelf = KnowledgeShelf::new();
        shelf.upsert(record.clone());
        let results = shelf.search("borrowing", |candidate| {
            (candidate.id == record.id).then(|| "# Borrowing\nLocal export body".to_owned())
        });
        let output = format_shelf_search("borrowing", ShelfSearchFormat::Links, &results)
            .replace(&record.id, "shelf-<id>");

        assert_eq!(
            output.trim_end(),
            include_str!("../tests/golden/shelf-search.links").trim_end()
        );
    }

    #[test]
    fn adapter_check_reports_generic_fallback() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let result = run_with_args(
            vec![
                "adapter".to_owned(),
                "check".to_owned(),
                workspace_path("crates/index-transformer/tests/fixtures/article.html"),
            ],
            &mut stdin,
        );

        assert!(matches!(result, Ok(CliAction::Print(_))));
        if let Ok(CliAction::Print(report)) = result {
            assert!(report.contains("adapter: none"));
            assert!(report.contains("support_tier: 2"));
            assert!(report.contains("fallback_reason:"));
            assert!(report.contains("links:"));
            assert!(report.contains("forms: 0"));
        }
    }

    #[test]
    fn adapter_scaffold_and_diff_manifest_reports_stable_contract()
    -> Result<(), Box<dyn std::error::Error>> {
        let fixture =
            workspace_path("crates/index-transformer/tests/fixtures/adapters/mdn-reference.html");
        let manifest_path = unique_temp_file("adapter-manifest");
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let scaffold = run_with_args(
            vec!["adapter".to_owned(), "scaffold".to_owned(), fixture.clone()],
            &mut stdin,
        )?;
        let CliAction::Print(manifest) = scaffold else {
            return Err("adapter scaffold did not print manifest".into());
        };
        assert!(manifest.starts_with("index-adapter-manifest-v1"));
        assert!(manifest.contains("adapter: mdn"));
        assert!(manifest.contains("support_tier: 3"));
        fs::write(&manifest_path, manifest)?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let diff = run_with_args(
            vec![
                "adapter".to_owned(),
                "diff".to_owned(),
                fixture,
                manifest_path.display().to_string(),
            ],
            &mut stdin,
        )?;
        assert!(
            matches!(diff, CliAction::Print(output) if output.contains("index-adapter-diff-v1") && output.contains("status: ok"))
        );

        fs::remove_file(&manifest_path)?;
        Ok(())
    }

    #[test]
    fn adapter_diff_reports_manifest_mismatches() -> Result<(), Box<dyn std::error::Error>> {
        let fixture =
            workspace_path("crates/index-transformer/tests/fixtures/adapters/mdn-reference.html");
        let manifest_path = unique_temp_file("adapter-manifest-bad");
        fs::write(
            &manifest_path,
            "index-adapter-manifest-v1\nadapter: none\nsupport_tier: 0\nnodes: 0\nlinks: 0\nforms: 0\ntables: 0\nregions: 0\nmarkdown_contains: missing text\n",
        )?;

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let diff = run_with_args(
            vec![
                "adapter".to_owned(),
                "diff".to_owned(),
                fixture,
                manifest_path.display().to_string(),
            ],
            &mut stdin,
        )?;
        assert!(
            matches!(diff, CliAction::Print(output) if output.contains("status: changed") && output.contains("mismatch: adapter expected none actual mdn") && output.contains("markdown_contains missing"))
        );

        fs::remove_file(&manifest_path)?;
        Ok(())
    }

    #[test]
    fn adapter_command_reports_help_and_errors() {
        let mut stdin = Cursor::new(Vec::<u8>::new());
        let help = run_with_args(vec!["adapter".to_owned()], &mut stdin);
        assert!(matches!(help, Ok(CliAction::Help(_))));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let check_help = run_with_args(vec!["adapter".to_owned(), "check".to_owned()], &mut stdin);
        assert!(matches!(check_help, Ok(CliAction::Help(_))));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let too_many = run_with_args(
            vec![
                "adapter".to_owned(),
                "check".to_owned(),
                "one".to_owned(),
                "two".to_owned(),
            ],
            &mut stdin,
        );
        assert!(matches!(too_many, Err(error) if error == "too many arguments"));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let diff_help = run_with_args(vec!["adapter".to_owned(), "diff".to_owned()], &mut stdin);
        assert!(matches!(diff_help, Ok(CliAction::Help(_))));

        let mut stdin = Cursor::new(Vec::<u8>::new());
        let unsupported =
            run_with_args(vec!["adapter".to_owned(), "unknown".to_owned()], &mut stdin);
        assert!(matches!(unsupported, Err(error) if error.contains("unsupported adapter command")));
    }
}
