git-slop 0.16.4

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
use std::fs;
use std::process::Command;

use git_slop::run_find_in;
use serde_json::Value;
use tempfile::TempDir;

fn git(repository: &TempDir, args: &[&str]) {
    let output = Command::new("git")
        .current_dir(repository.path())
        .args(args)
        .output()
        .expect("run git");
    assert!(
        output.status.success(),
        "git {} failed: {}",
        args.join(" "),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn commit_all(repository: &TempDir, message: &str) {
    git(repository, &["add", "--all"]);
    git(
        repository,
        &["-c", "commit.gpgsign=false", "commit", "-m", message],
    );
}

fn duplicate_source(revision: usize) -> String {
    let block = [
        "pub fn shared_logic(value: &str) -> Vec<String> {",
        "    let normalized = value.trim().to_ascii_lowercase();",
        "    normalized.split_whitespace().map(str::to_owned).collect()",
        "}",
    ]
    .join("\n");
    format!(
        "{}\n// paired revision {revision}\n",
        vec![block; 30].join("\n")
    )
}

fn write_duplicate_pair(repository: &TempDir, left: &str, right: &str, revision: usize) {
    let source = duplicate_source(revision);
    fs::write(repository.path().join(left), &source).expect("write left duplicate");
    fs::write(repository.path().join(right), source).expect("write right duplicate");
}

fn relationship_for_pair<'a>(
    report: &'a Value,
    kind: &str,
    left: &str,
    right: &str,
) -> Option<&'a Value> {
    report["overlays"]["organization_health"]["relationships"][kind]
        .as_array()?
        .iter()
        .find(|relationship| {
            relationship["source_path"] == left && relationship["target_path"] == right
        })
}

fn cluster_contains_pair(cluster: &Value, left: &str, right: &str) -> bool {
    cluster["member_paths"].as_array().is_some_and(|paths| {
        paths.contains(&Value::String(left.to_string()))
            && paths.contains(&Value::String(right.to_string()))
    })
}

fn record_for_path<'a>(records: &'a Value, path: &str) -> &'a Value {
    records
        .as_array()
        .expect("record array")
        .iter()
        .find(|record| record["path"] == path)
        .unwrap_or_else(|| panic!("missing record for {path}"))
}

fn initialize_repository() -> TempDir {
    let repository = TempDir::new().expect("temporary repository");
    git(&repository, &["init", "-b", "main"]);
    git(&repository, &["config", "user.name", "Git Slop Tests"]);
    git(
        &repository,
        &["config", "user.email", "git-slop-tests@example.invalid"],
    );
    fs::write(repository.path().join(".gitignore"), ".slop/\n").expect("gitignore");
    for directory in ["src", "pkg", "docs"] {
        fs::create_dir_all(repository.path().join(directory)).expect("fixture directory");
    }
    repository
}

#[test]
fn native_detector_emits_duplicate_coupling_clusters_and_refreshes_latest() {
    let repository = initialize_repository();
    let original_left = "src/alpha.rs";
    let original_right = "pkg/beta.rs";
    let obsolete = "docs/obsolete.md";

    write_duplicate_pair(&repository, original_left, original_right, 0);
    fs::write(
        repository.path().join(obsolete),
        "# Obsolete\n\nThis tracked file should disappear from the refreshed report.\n",
    )
    .expect("obsolete fixture");
    commit_all(&repository, "initial structure");

    for revision in 1..=3 {
        write_duplicate_pair(&repository, original_left, original_right, revision);
        commit_all(&repository, &format!("paired change {revision}"));
    }

    let first = run_find_in(repository.path()).expect("run native detector");
    assert!(
        first.report["diagnostics"]["analysis"]["cache_misses"]
            .as_u64()
            .unwrap_or_default()
            > 0
    );
    let report = &first.report;
    let duplicate = relationship_for_pair(
        report,
        "duplicate_neighborhoods",
        original_right,
        original_left,
    )
    .expect("exact duplicate relationship");
    assert_eq!(duplicate["kind"], "duplicate_neighborhood");
    assert_eq!(duplicate["similarity"], 1.0);
    assert!(
        relationship_for_pair(
            report,
            "near_duplicate_neighborhoods",
            original_right,
            original_left,
        )
        .is_none()
    );

    let coupling = relationship_for_pair(
        report,
        "temporal_coupling_edges",
        original_right,
        original_left,
    )
    .expect("temporal coupling relationship");
    assert!(coupling["support_count"].as_u64().unwrap_or_default() >= 3);
    assert!(coupling["evidence_score"].as_f64().unwrap_or_default() > 0.0);

    for cluster_kind in ["duplicate_sets", "consolidation_candidates"] {
        assert!(
            report["overlays"]["organization_health"]["clusters"][cluster_kind]
                .as_array()
                .expect("cluster array")
                .iter()
                .any(|cluster| cluster_contains_pair(cluster, original_right, original_left)),
            "missing {cluster_kind} for the duplicate pair"
        );
    }

    let left_file = record_for_path(&report["files"], original_left);
    let right_file = record_for_path(&report["files"], original_right);
    assert!(left_file.get("structural_tokens").is_none());
    assert!(right_file.get("structural_tokens").is_none());
    assert!(left_file["content_fingerprint"].as_str().is_some());
    assert!(right_file["content_fingerprint"].as_str().is_some());

    let organization = record_for_path(
        &report["overlays"]["organization_health"]["files"],
        original_left,
    );
    assert!(
        organization["duplication_pressure"]
            .as_f64()
            .unwrap_or_default()
            > 0.0
    );
    assert!(
        organization["coupling_pressure"]
            .as_f64()
            .unwrap_or_default()
            > 0.0
    );
    assert!(
        organization["boundary_pressure"]
            .as_f64()
            .unwrap_or_default()
            > 0.0
    );
    assert!(
        organization["cross_boundary_edge_count"]
            .as_u64()
            .unwrap_or_default()
            > 0
    );
    assert!(
        organization["relationship_ids"]
            .as_array()
            .is_some_and(|ids| !ids.is_empty())
    );
    assert!(
        organization["cluster_ids"]
            .as_array()
            .is_some_and(|ids| !ids.is_empty())
    );
    let blast_radius = record_for_path(&report["overlays"]["blast_radius"]["files"], original_left);
    assert!(
        blast_radius["blast_radius_pressure"]
            .as_f64()
            .unwrap_or_default()
            > 0.0
    );

    let first_latest = fs::read_to_string(repository.path().join(".slop/latest/report.json"))
        .expect("first latest report");
    assert!(first_latest.contains(original_left));
    assert!(first_latest.contains(obsolete));
    fs::write(
        repository.path().join(".slop/latest/stale-only.txt"),
        "must be removed by the next atomic latest refresh\n",
    )
    .expect("stale latest sentinel");

    let renamed_left = "engine/renamed_alpha.rs";
    fs::create_dir_all(repository.path().join("engine")).expect("renamed fixture directory");
    git(&repository, &["mv", original_left, renamed_left]);
    git(&repository, &["rm", obsolete]);
    commit_all(&repository, "rename duplicate and remove obsolete file");

    let second = run_find_in(repository.path()).expect("rerun native detector");
    assert!(
        second.report["diagnostics"]["analysis"]["cache_hits"]
            .as_u64()
            .unwrap_or_default()
            > 0,
        "unchanged files should reuse the content-addressed token cache"
    );
    assert!(record_for_path(&second.report["files"], renamed_left).is_object());
    assert!(
        relationship_for_pair(
            &second.report,
            "duplicate_neighborhoods",
            renamed_left,
            original_right,
        )
        .is_some()
    );
    for artifact in ["report.json", "summary.md", "health.md"] {
        let contents = fs::read_to_string(repository.path().join(".slop/latest").join(artifact))
            .unwrap_or_else(|error| panic!("read refreshed {artifact}: {error}"));
        assert!(
            !contents.contains(original_left),
            "stale rename in {artifact}"
        );
        assert!(!contents.contains(obsolete), "stale deletion in {artifact}");
    }
    assert!(!repository.path().join(".slop/latest/report.yaml").exists());
    assert!(
        !repository
            .path()
            .join(".slop/latest/stale-only.txt")
            .exists(),
        "atomic latest refresh retained an obsolete artifact"
    );
}