git-slop 0.16.1

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
fn require_compatible_reports(mismatches: &[Value]) -> Result<()> {
    if let Some(first) = mismatches
        .iter()
        .find(|mismatch| mismatch.get("blocking").and_then(Value::as_bool) != Some(false))
    {
        let label = string(first.get("field"));
        let base = first.get("base").cloned().unwrap_or(Value::Null);
        let head = first.get("head").cloned().unwrap_or(Value::Null);
        bail!(
            "reports have incompatible {label}: base={base}, head={head}; rerun compare with --force only if this mismatch is intentional"
        );
    }
    Ok(())
}

fn has_blocking_mismatches(mismatches: &[Value]) -> bool {
    mismatches
        .iter()
        .any(|mismatch| mismatch.get("blocking").and_then(Value::as_bool) != Some(false))
}

fn require_comparison_ready(
    report: &Value,
    role: &str,
    allow_incomplete_evidence: bool,
    force: bool,
) -> Result<()> {
    let readiness =
        evaluate_report_readiness(report, role == "base" && !force, allow_incomplete_evidence);
    if let Some(blocker) = readiness.blockers.first() {
        let code = string(blocker.get("code"));
        let pointer = string(blocker.get("pointer"));
        let message = string(blocker.get("message"));
        bail!(
            "{role} report is not comparison-ready ({code} at {pointer}): {message}; rerun `git slop find` with complete inputs"
        );
    }
    Ok(())
}

fn build_record_delta(path: &str, base: Option<&Value>, head: Option<&Value>) -> Value {
    let base_score = record_score(base);
    let head_score = record_score(head);
    let base_tokens = record_tokens(base);
    let head_tokens = record_tokens(head);
    let base_load = record_load_pressure(base);
    let head_load = record_load_pressure(head);
    let base_context = record_band(base, "context_band");
    let head_context = record_band(head, "context_band");
    let base_slop = record_band(base, "slop_band");
    let head_slop = record_band(head, "slop_band");
    let base_fingerprint = base.and_then(record_content_identity);
    let head_fingerprint = head.and_then(record_content_identity);
    let content_changed = match (&base_fingerprint, &head_fingerprint) {
        (Some(base), Some(head)) => Some(base != head),
        _ => None,
    };
    let metrics_changed = match (base, head) {
        (Some(base), Some(head)) => Some(metric_changed(base, head)),
        _ => None,
    };
    let content_status = match (base, head, content_changed) {
        (None, Some(_), _) => "added",
        (Some(_), None, _) => "removed",
        (Some(_), Some(_), Some(true)) => "changed",
        (Some(_), Some(_), Some(false)) => "unchanged",
        _ => "unknown",
    };
    let metric_status = match metrics_changed {
        Some(true) => "changed",
        Some(false) => "unchanged",
        None => "not_comparable",
    };
    let evidence_status = match (content_changed, metrics_changed) {
        (Some(false), Some(true)) => "drifted",
        (Some(_), Some(_)) => "stable",
        _ => "not_comparable",
    };
    let context_delta = band_delta(base_context.as_deref(), head_context.as_deref());
    let slop_delta = band_delta(base_slop.as_deref(), head_slop.as_deref());
    json!({
        "path": path,
        "status": record_delta_status(base, head),
        "content_status": content_status,
        "metric_status": metric_status,
        "evidence_status": evidence_status,
        "base_slop_score": base_score,
        "head_slop_score": head_score,
        "slop_score_delta": option_f64_delta(base_score, head_score),
        "base_tokens": base_tokens,
        "head_tokens": head_tokens,
        "token_delta": option_i64_delta(base_tokens, head_tokens),
        "base_load_pressure": base_load,
        "head_load_pressure": head_load,
        "load_pressure_delta": option_f64_delta(base_load, head_load),
        "base_context_band": base_context,
        "head_context_band": head_context,
        "context_band_delta": context_delta,
        "base_slop_band": base_slop,
        "head_slop_band": head_slop,
        "slop_band_delta": slop_delta,
        "base_content_fingerprint": base_fingerprint,
        "head_content_fingerprint": head_fingerprint,
        "content_changed": content_changed,
        "overlay_deltas": record_overlay_delta(base, head),
    })
}

fn regression_for_delta(delta: &Value, base: &Value) -> Option<Value> {
    let path = string(delta.get("path"));
    let status = string(delta.get("status"));
    let head_context_band = string(delta.get("head_context_band"));
    let head_slop_band = string(delta.get("head_slop_band"));
    let severity = if matches!(
        head_context_band.as_str(),
        "critical" | "refactor_required" | "budget_exceeded"
    ) || head_slop_band == "critical"
    {
        "error"
    } else if head_context_band == "warning" || head_slop_band == "high" {
        "warning"
    } else {
        "notice"
    };
    let reason = if status == "added" {
        if !matches!(
            head_context_band.as_str(),
            "warning" | "critical" | "refactor_required" | "budget_exceeded"
        ) && !matches!(head_slop_band.as_str(), "high" | "critical")
        {
            return None;
        }
        "new_finding"
    } else if matches!(status.as_str(), "source_changed" | "evidence_drift") {
        let context_delta = delta
            .get("context_band_delta")
            .and_then(Value::as_i64)
            .unwrap_or_default();
        let slop_delta = delta
            .get("slop_band_delta")
            .and_then(Value::as_i64)
            .unwrap_or_default();
        let score_delta = number(delta.get("slop_score_delta"));
        let threshold = base
            .pointer("/config/check/regression_score_delta")
            .and_then(Value::as_f64)
            .unwrap_or(5.0);
        let content_changed = delta
            .get("content_changed")
            .and_then(Value::as_bool)
            .unwrap_or(true);
        let fail_on_evidence_drift = base
            .pointer("/config/check/fail_on_evidence_drift")
            .and_then(Value::as_bool)
            .unwrap_or(false);
        if status == "evidence_drift" && !fail_on_evidence_drift {
            return None;
        }
        if (context_delta > 0 || slop_delta > 0) && (content_changed || fail_on_evidence_drift) {
            "worse_band"
        } else if score_delta >= threshold && content_changed {
            "material_score_increase"
        } else {
            return None;
        }
    } else {
        return None;
    };
    Some(json!({
        "path": path,
        "status": if status == "added" { "new" } else { "worsened" },
        "reason": reason,
        "severity": severity,
        "base_slop_score": delta.get("base_slop_score").cloned().unwrap_or(Value::Null),
        "head_slop_score": delta.get("head_slop_score").cloned().unwrap_or(Value::Null),
        "slop_score_delta": delta.get("slop_score_delta").cloned().unwrap_or(Value::Null),
        "context_band": delta.get("head_context_band").cloned().unwrap_or(Value::Null),
        "slop_band": delta.get("head_slop_band").cloned().unwrap_or(Value::Null),
    }))
}

fn build_record_deltas(base: &Value, head: &Value, collection: &str) -> Vec<Value> {
    let base_records = records_by_path(base, collection);
    let head_records = records_by_path(head, collection);
    let fingerprint_paths = |records: &BTreeMap<String, Value>, other: &BTreeMap<String, Value>| {
        let mut values: BTreeMap<String, Vec<String>> = BTreeMap::new();
        for (path, record) in records {
            if other.contains_key(path) {
                continue;
            }
            if let Some(fingerprint) = record_content_identity(record) {
                values.entry(fingerprint).or_default().push(path.clone());
            }
        }
        values
    };
    let base_fingerprints = fingerprint_paths(&base_records, &head_records);
    let head_fingerprints = fingerprint_paths(&head_records, &base_records);
    let mut renamed_base_paths = BTreeSet::new();
    let mut renamed_head_paths = BTreeSet::new();
    let mut renamed = Vec::new();
    for (fingerprint, old_paths) in &base_fingerprints {
        let Some(new_paths) = head_fingerprints.get(fingerprint) else {
            continue;
        };
        if old_paths.len() != 1 || new_paths.len() != 1 {
            continue;
        }
        let old_path = &old_paths[0];
        let new_path = &new_paths[0];
        renamed_base_paths.insert(old_path.clone());
        renamed_head_paths.insert(new_path.clone());
        let mut delta = build_record_delta(
            new_path,
            base_records.get(old_path),
            head_records.get(new_path),
        );
        delta["status"] = json!("renamed");
        delta["content_status"] = json!("renamed_unchanged");
        delta["renamed_from"] = json!(old_path);
        delta["renamed_to"] = json!(new_path);
        renamed.push(delta);
    }
    let paths: BTreeSet<String> = base_records
        .keys()
        .chain(head_records.keys())
        .filter(|path| !renamed_base_paths.contains(*path) && !renamed_head_paths.contains(*path))
        .cloned()
        .collect();
    let mut deltas = paths
        .into_iter()
        .map(|path| {
            let mut delta =
                build_record_delta(&path, base_records.get(&path), head_records.get(&path));
            if collection == "folders"
                && delta.get("status").and_then(Value::as_str) == Some("source_changed")
                && delta.get("content_status").and_then(Value::as_str) == Some("unknown")
            {
                delta["status"] = json!("aggregate_changed");
                delta["content_status"] = json!("not_applicable");
            }
            delta
        })
        .collect::<Vec<_>>();
    deltas.extend(renamed);
    deltas.sort_by_key(|left| string(left.get("path")));
    deltas
}

fn queue_positions(report: &Value) -> BTreeMap<String, usize> {
    let mut positions = BTreeMap::new();
    for (index, item) in array_at(report, &["action_queue"]).iter().enumerate() {
        if let Some(path) = item.get("path").and_then(Value::as_str) {
            positions.entry(path.to_string()).or_insert(index + 1);
        }
    }
    positions
}

fn build_queue_movement(base: &Value, head: &Value) -> Vec<Value> {
    let base_positions = queue_positions(base);
    let head_positions = queue_positions(head);
    let paths: BTreeSet<String> = base_positions
        .keys()
        .chain(head_positions.keys())
        .cloned()
        .collect();
    let mut result: Vec<Value> = paths
        .into_iter()
        .map(|path| {
            let base_position = base_positions.get(&path).copied();
            let head_position = head_positions.get(&path).copied();
            let (status, position_delta): (&str, Option<i64>) = match (base_position, head_position)
            {
                (None, Some(_)) => ("newly_queued", None),
                (Some(_), None) => ("dropped_from_queue", None),
                (Some(base), Some(head)) if head < base => {
                    ("moved_up", Some(head as i64 - base as i64))
                }
                (Some(base), Some(head)) if head > base => {
                    ("moved_down", Some(head as i64 - base as i64))
                }
                _ => ("unchanged_position", Some(0)),
            };
            json!({
                "path": path,
                "status": status,
                "base_position": base_position,
                "head_position": head_position,
                "position_delta": position_delta,
            })
        })
        .collect();
    result.sort_by(|left, right| {
        let rank = |status: &str| match status {
            "newly_queued" => 0,
            "moved_up" => 1,
            "moved_down" => 2,
            "dropped_from_queue" => 3,
            _ => 4,
        };
        rank(&string(left.get("status")))
            .cmp(&rank(&string(right.get("status"))))
            .then_with(|| {
                let left_position = left
                    .get("head_position")
                    .and_then(Value::as_u64)
                    .or_else(|| left.get("base_position").and_then(Value::as_u64))
                    .and_then(|value| usize::try_from(value).ok())
                    .unwrap_or(10_000);
                let right_position = right
                    .get("head_position")
                    .and_then(Value::as_u64)
                    .or_else(|| right.get("base_position").and_then(Value::as_u64))
                    .and_then(|value| usize::try_from(value).ok())
                    .unwrap_or(10_000);
                left_position.cmp(&right_position)
            })
            .then_with(|| string(left.get("path")).cmp(&string(right.get("path"))))
    });
    result
}

fn delta_counts(items: &[Value]) -> Value {
    let count = |status: &str| {
        items
            .iter()
            .filter(|item| string(item.get("status")) == status)
            .count()
    };
    json!({
        "added": count("added"),
        "removed": count("removed"),
        "changed": count("source_changed") + count("evidence_drift") + count("aggregate_changed"),
        "source_changed": count("source_changed"),
        "evidence_drift": count("evidence_drift"),
        "renamed": count("renamed"),
        "aggregate_changed": count("aggregate_changed"),
        "unchanged": count("unchanged"),
    })
}

fn aggregate_overlay_deltas(items: &[Value]) -> Vec<Value> {
    let mut aggregate: BTreeMap<String, f64> = BTreeMap::new();
    for item in items {
        for overlay in array_at(item, &["overlay_deltas"]) {
            let label = string(overlay.get("label"));
            let delta = number(overlay.get("delta"));
            aggregate
                .entry(label)
                .and_modify(|value| *value = round6(*value + delta))
                .or_insert(delta);
        }
    }
    let mut result: Vec<Value> = aggregate
        .into_iter()
        .filter(|(_, delta)| *delta != 0.0)
        .map(|(label, total_delta)| json!({"label": label, "total_delta": total_delta}))
        .collect();
    result.sort_by(|left, right| {
        cmp_f64_desc(
            number(left.get("total_delta")).abs(),
            number(right.get("total_delta")).abs(),
        )
        .then_with(|| string(left.get("label")).cmp(&string(right.get("label"))))
    });
    result
}

fn report_descriptor(report: &Value, path: Option<&str>) -> Value {
    let repo = report.get("repo").unwrap_or(&Value::Null);
    let report_digest = serde_json::to_vec(report)
        .map(|bytes| hex::encode(Sha256::digest(bytes)))
        .unwrap_or_default();
    json!({
        "path": path,
        "repo_name": repo.get("repo_name").cloned().unwrap_or(Value::Null),
        "head_sha": repo.get("head_sha").or_else(|| repo.get("head_commit")).cloned().unwrap_or(Value::Null),
        "generated_at": report.get("generated_at")
            .or_else(|| value_at(report, &["summary", "generated_at"]))
            .cloned()
            .unwrap_or(Value::Null),
        "analyzed_revision_at": report.get("analyzed_revision_at").cloned().unwrap_or(Value::Null),
        "schema_version": report.get("schema_version").cloned().unwrap_or(Value::Null),
        "report_digest": report_digest,
        "content_digest": repo.get("analyzed_content_digest").cloned().unwrap_or(Value::Null),
        "scope": report.get("scope").cloned().unwrap_or(Value::Null),
        "analyzer_version": report.pointer("/analyzer/version").cloned().unwrap_or(Value::Null),
        "analysis_contract_version": report.pointer("/analyzer/analysis_contract_version").cloned().unwrap_or(Value::Null),
        "config_digest": report.pointer("/analyzer/config_digest").cloned().unwrap_or(Value::Null),
        "analysis_config_digest": report.pointer("/analyzer/analysis_config_digest").cloned().unwrap_or(Value::Null),
        "context_tokenizer": report.pointer("/analyzer/context_tokenizer").cloned().unwrap_or(Value::Null),
        "history_complete": report.pointer("/stats/history_complete").cloned().unwrap_or(Value::Null),
    })
}