tokmd 1.11.0

Tokei-backed repo inventory receipts (Markdown/TSV/JSONL/CSV) for PRs, CI, and LLM workflows.
Documentation
use crate::cli;
use anyhow::Result;
use tokmd_analysis as analysis;
use tokmd_format::badge_svg;

use crate::analysis_utils;
use crate::export_bundle;

pub(crate) fn handle(args: cli::BadgeArgs, global: &cli::GlobalArgs) -> Result<()> {
    let metric = args.metric;
    let mut preset = args.preset.unwrap_or(cli::AnalysisPreset::Receipt);
    if metric == cli::BadgeMetric::Hotspot && args.preset.is_none() {
        preset = cli::AnalysisPreset::Risk;
    }
    let git_flag = if args.git {
        Some(true)
    } else if args.no_git {
        Some(false)
    } else if metric == cli::BadgeMetric::Hotspot {
        Some(true)
    } else {
        None
    };

    let bundle = export_bundle::load_export_from_inputs(&args.inputs, global)?;
    let source = tokmd_analysis_types::AnalysisSource {
        inputs: args
            .inputs
            .iter()
            .map(|p| p.display().to_string())
            .collect(),
        export_path: bundle.export_path.as_ref().map(|p| p.display().to_string()),
        base_receipt_path: bundle.export_path.as_ref().map(|p| p.display().to_string()),
        export_schema_version: bundle.meta.schema_version,
        export_generated_at_ms: bundle.meta.generated_at_ms,
        base_signature: None,
        module_roots: bundle.meta.module_roots.clone(),
        module_depth: bundle.meta.module_depth,
        children: analysis_utils::child_include_to_string(bundle.meta.children),
    };
    let args_meta = tokmd_analysis_types::AnalysisArgsMeta {
        preset: analysis_utils::preset_to_string(preset),
        format: "badge".to_string(),
        window_tokens: None,
        git: git_flag,
        max_files: None,
        max_bytes: None,
        max_file_bytes: None,
        max_commits: args.max_commits,
        max_commit_files: args.max_commit_files,
        import_granularity: "module".to_string(),
    };
    let request = analysis::AnalysisRequest {
        preset: analysis_utils::map_preset(preset),
        args: args_meta,
        limits: analysis::AnalysisLimits {
            max_files: None,
            max_bytes: None,
            max_file_bytes: None,
            max_commits: args.max_commits,
            max_commit_files: args.max_commit_files,
        },
        window_tokens: None,
        git: git_flag,
        import_granularity: analysis::ImportGranularity::Module,
        detail_functions: false,
        near_dup: false,
        near_dup_threshold: 0.80,
        near_dup_max_files: 2000,
        near_dup_scope: analysis::NearDupScope::Module,
        near_dup_max_pairs: None,
        near_dup_exclude: Vec::new(),
        effort: None,
    };
    let ctx = analysis::AnalysisContext {
        export: bundle.export,
        root: bundle.root,
        source,
    };
    let receipt = analysis::analyze(ctx, request)?;

    let value = match metric {
        cli::BadgeMetric::Lines => receipt
            .derived
            .as_ref()
            .map(|d| d.totals.lines.to_string())
            .unwrap_or_else(|| "0".to_string()),
        cli::BadgeMetric::Tokens => receipt
            .derived
            .as_ref()
            .map(|d| d.totals.tokens.to_string())
            .unwrap_or_else(|| "0".to_string()),
        cli::BadgeMetric::Bytes => receipt
            .derived
            .as_ref()
            .map(|d| d.totals.bytes.to_string())
            .unwrap_or_else(|| "0".to_string()),
        cli::BadgeMetric::Doc => receipt
            .derived
            .as_ref()
            .map(|d| format!("{:.1}%", d.doc_density.total.ratio * 100.0))
            .unwrap_or_else(|| "0%".to_string()),
        cli::BadgeMetric::Blank => receipt
            .derived
            .as_ref()
            .map(|d| format!("{:.1}%", d.whitespace.total.ratio * 100.0))
            .unwrap_or_else(|| "0%".to_string()),
        cli::BadgeMetric::Hotspot => receipt
            .git
            .as_ref()
            .and_then(|g| g.hotspots.first())
            .map(|h| h.score.to_string())
            .unwrap_or_else(|| "n/a".to_string()),
    };

    let label = badge_metric_label(metric);
    let svg = badge_svg(label, &value);

    if let Some(output) = args.output {
        std::fs::write(output, svg)?;
    } else {
        print!("{}", svg);
    }

    Ok(())
}

fn badge_metric_label(metric: cli::BadgeMetric) -> &'static str {
    match metric {
        cli::BadgeMetric::Lines => "lines",
        cli::BadgeMetric::Tokens => "tokens",
        cli::BadgeMetric::Bytes => "bytes",
        cli::BadgeMetric::Doc => "doc",
        cli::BadgeMetric::Blank => "blank",
        cli::BadgeMetric::Hotspot => "hotspot",
    }
}