tokmd-analysis 1.10.0

Analysis logic and enrichers for tokmd receipts.
Documentation
#[cfg(feature = "git")]
mod git_tests {
    use std::process::Command;

    use tempfile::tempdir;
    use tokmd_analysis::{
        AnalysisContext, AnalysisLimits, AnalysisPreset, AnalysisRequest, ImportGranularity,
        NearDupScope, analyze,
    };
    use tokmd_analysis_types::{AnalysisArgsMeta, AnalysisSource};
    use tokmd_types::{ChildIncludeMode, ExportData, FileKind, FileRow};

    fn git_cmd(dir: &std::path::Path, args: &[&str]) {
        let status = Command::new("git")
            .arg("-C")
            .arg(dir)
            .args(args)
            .status()
            .expect("git command failed");
        assert!(status.success());
    }

    #[test]
    fn git_metrics_basic() {
        if !tokmd_git::git_available() {
            return;
        }

        let dir = tempdir().expect("tempdir");
        let root = dir.path();
        git_cmd(root, &["init"]);

        std::fs::create_dir_all(root.join("src")).unwrap();
        std::fs::write(root.join("src/lib.rs"), "fn a() {}\n").unwrap();

        let mut cmd = Command::new("git");
        cmd.arg("-C")
            .arg(root)
            .args(["add", "."])
            .status()
            .expect("git add");

        let mut cmd = Command::new("git");
        cmd.arg("-C")
            .arg(root)
            .args(["commit", "-m", "first"])
            .env("GIT_AUTHOR_NAME", "Alice")
            .env("GIT_AUTHOR_EMAIL", "alice@example.com")
            .env("GIT_AUTHOR_DATE", "2020-01-01T00:00:00Z")
            .env("GIT_COMMITTER_NAME", "Alice")
            .env("GIT_COMMITTER_EMAIL", "alice@example.com")
            .env("GIT_COMMITTER_DATE", "2020-01-01T00:00:00Z")
            .status()
            .expect("git commit");

        std::fs::write(root.join("src/lib.rs"), "fn a() {}\nfn b() {}\n").unwrap();
        std::fs::write(root.join("README.md"), "# test\n").unwrap();

        git_cmd(root, &["add", "."]);

        let mut cmd = Command::new("git");
        cmd.arg("-C")
            .arg(root)
            .args(["commit", "-m", "second"])
            .env("GIT_AUTHOR_NAME", "Bob")
            .env("GIT_AUTHOR_EMAIL", "bob@example.com")
            .env("GIT_AUTHOR_DATE", "2020-01-02T00:00:00Z")
            .env("GIT_COMMITTER_NAME", "Bob")
            .env("GIT_COMMITTER_EMAIL", "bob@example.com")
            .env("GIT_COMMITTER_DATE", "2020-01-02T00:00:00Z")
            .status()
            .expect("git commit");

        let export = ExportData {
            rows: vec![
                FileRow {
                    path: "src/lib.rs".to_string(),
                    module: "src".to_string(),
                    lang: "Rust".to_string(),
                    kind: FileKind::Parent,
                    code: 2,
                    comments: 0,
                    blanks: 0,
                    lines: 2,
                    bytes: 20,
                    tokens: 5,
                },
                FileRow {
                    path: "README.md".to_string(),
                    module: "(root)".to_string(),
                    lang: "Markdown".to_string(),
                    kind: FileKind::Parent,
                    code: 1,
                    comments: 0,
                    blanks: 0,
                    lines: 1,
                    bytes: 8,
                    tokens: 2,
                },
            ],
            module_roots: vec!["crates".to_string(), "packages".to_string()],
            module_depth: 2,
            children: ChildIncludeMode::Separate,
        };

        let ctx = AnalysisContext {
            export,
            root: root.to_path_buf(),
            source: AnalysisSource {
                inputs: vec![root.display().to_string()],
                export_path: None,
                base_receipt_path: None,
                export_schema_version: None,
                export_generated_at_ms: None,
                base_signature: None,
                module_roots: vec!["crates".to_string(), "packages".to_string()],
                module_depth: 2,
                children: "separate".to_string(),
            },
        };

        let request = AnalysisRequest {
            preset: AnalysisPreset::Risk,
            args: AnalysisArgsMeta {
                preset: "risk".to_string(),
                format: "md".to_string(),
                window_tokens: None,
                git: Some(true),
                max_files: None,
                max_bytes: None,
                max_file_bytes: None,
                max_commits: None,
                max_commit_files: None,
                import_granularity: "module".to_string(),
            },
            limits: AnalysisLimits::default(),
            window_tokens: None,
            git: Some(true),
            import_granularity: ImportGranularity::Module,
            detail_functions: false,
            near_dup: false,
            near_dup_threshold: 0.80,
            near_dup_max_files: 2000,
            near_dup_scope: NearDupScope::Module,
            near_dup_max_pairs: None,
            near_dup_exclude: Vec::new(),
            #[cfg(feature = "effort")]
            effort: None,
        };

        let receipt = analyze(ctx, request).expect("analysis");
        let git = receipt.git.expect("git report");
        assert_eq!(git.commits_scanned, 2);
        assert!(!git.hotspots.is_empty());
        assert!(!git.bus_factor.is_empty());
        let age = git.age_distribution.expect("age distribution");
        assert_eq!(age.recent_refreshes, 2);
        assert_eq!(age.prior_refreshes, 0);
        assert_eq!(age.refresh_trend, tokmd_analysis_types::TrendClass::Rising);
        assert!(!age.buckets.is_empty());
    }
}