git-slop 0.10.1

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
use super::*;
use sha2::{Digest, Sha256};

fn sarif_level(record: &Value) -> &'static str {
    let slop_band = string(record.get("slop_band"));
    let context_band = string(record.get("context_band"));
    if slop_band == "critical" || context_band == "critical" || context_band == "refactor_required"
    {
        "error"
    } else if matches!(slop_band.as_str(), "high" | "moderate") || context_band == "warning" {
        "warning"
    } else {
        "note"
    }
}

fn sarif_record(report: &Value, queue_item: &Value) -> Value {
    let path = string(queue_item.get("path"));
    let mut record = find_record(report, &path)
        .map(|(record, _)| record)
        .unwrap_or_else(|| queue_item.clone());
    let record_object = record.as_object_mut().expect("record object");
    for key in ["slop_score", "slop_band", "context_band", "reason_codes"] {
        if !record_object.contains_key(key) {
            record_object.insert(
                key.to_string(),
                queue_item.get(key).cloned().unwrap_or_else(|| {
                    if key == "reason_codes" {
                        json!([])
                    } else {
                        Value::Null
                    }
                }),
            );
        }
    }
    record
}

fn sarif_result(record: &Value, rank: usize) -> Value {
    let path = string(record.get("path"));
    let reasons = string_array(record.get("reason_codes"));
    let reasons_text = if reasons.is_empty() {
        "no reason codes".to_string()
    } else {
        reasons.join(", ")
    };
    let overlays: Map<String, Value> = strongest_pressures(record.get("overlays"), 8)
        .into_iter()
        .map(|(label, value)| (label, json!(round6(value))))
        .collect();
    let context_finding = matches!(
        string(record.get("context_band")).as_str(),
        "warning" | "critical" | "refactor_required"
    );
    let rule_id = if context_finding {
        "git-slop.context-budget"
    } else {
        "git-slop.maintenance-pressure"
    };
    let fingerprint = hex::encode(Sha256::digest(format!("{rule_id}\0{path}").as_bytes()));
    json!({
        "ruleId": rule_id,
        "ruleIndex": if context_finding { 0 } else { 1 },
        "level": sarif_level(record),
        "baselineState": record.get("baseline_state").and_then(Value::as_str).unwrap_or("unchanged"),
        "message": {
            "text": format!(
                "{path} is ranked {} with slop_score {} and context {} ({reasons_text}).",
                json_scalar_text(record.get("slop_band")),
                json_scalar_text(record.get("slop_score")),
                json_scalar_text(record.get("context_band")),
            ),
        },
        "locations": [{
            "physicalLocation": {
                "artifactLocation": {"uri": path},
            },
        }],
        "partialFingerprints": {"gitSlopFinding/v1": fingerprint},
        "properties": {
            "git_slop": {
                "rank": rank,
                "slop_score": record.get("slop_score").cloned().unwrap_or(Value::Null),
                "slop_band": record.get("slop_band").cloned().unwrap_or(Value::Null),
                "context_band": record.get("context_band").cloned().unwrap_or(Value::Null),
                "reason_codes": record.get("reason_codes").cloned().unwrap_or_else(|| json!([])),
                "costs": record.get("costs").cloned().unwrap_or_else(|| json!({})),
                "strongest_overlays": Value::Object(overlays),
                "evidence_boundary": "Hotspot cost and overlay evidence are preserved as separate properties; SARIF export does not rescore the finding.",
            },
        },
    })
}

pub fn sarif_payload(
    report: &Value,
    report_path: Option<&str>,
    top: Option<usize>,
) -> Result<Value> {
    require_report_schema(report, "sarif")?;
    if top == Some(0) {
        bail!("--top must be greater than zero.");
    }
    let queue = array_at(report, &["action_queue"]);
    let take = top.unwrap_or(queue.len());
    let results: Vec<Value> = queue
        .iter()
        .take(take)
        .enumerate()
        .filter_map(|(index, item)| {
            item.get("path")
                .and_then(Value::as_str)
                .map(|_| sarif_result(&sarif_record(report, item), index + 1))
        })
        .collect();
    let repo = report.get("repo").unwrap_or(&Value::Null);
    let repository_uri = repo
        .get("remote_url")
        .or_else(|| repo.get("git_remote_url"))
        .or_else(|| repo.get("repo_name"))
        .cloned()
        .unwrap_or(Value::Null);
    let revision_id = repo
        .get("head_sha")
        .or_else(|| repo.get("head_commit"))
        .cloned()
        .unwrap_or(Value::Null);
    Ok(json!({
        "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
        "version": "2.1.0",
        "runs": [{
            "tool": {
                "driver": {
                    "name": "git-slop",
                    "informationUri": "https://github.com/coreycoto/git-slop",
                    "rules": [{
                        "id": "git-slop.context-budget",
                        "name": "Git Slop context budget",
                        "shortDescription": {"text": "File meets a configured context-cost threshold."},
                        "fullDescription": {"text": "A deterministic context-cost finding from Git Slop."},
                        "helpUri": "https://github.com/coreycoto/git-slop/blob/main/docs/scoring-model.md",
                        "help": {"text": "Review the git-slop report, explain output, or plan output for supporting evidence before deciding whether maintenance work is appropriate."},
                        "properties": {
                            "precision": "medium",
                            "tags": ["maintainability", "context-cost", "git-slop"],
                        },
                    }, {
                        "id": "git-slop.maintenance-pressure",
                        "name": "Git Slop maintenance pressure",
                        "shortDescription": {"text": "File meets a configured maintenance-pressure threshold."},
                        "fullDescription": {"text": "A deterministic maintenance-pressure finding from Git Slop."},
                        "helpUri": "https://github.com/coreycoto/git-slop/blob/main/docs/scoring-model.md",
                        "properties": {"precision": "medium", "tags": ["maintainability", "git-slop"]}
                    }],
                },
            },
            "automationDetails": {"id": "git-slop/sarif"},
            "versionControlProvenance": [{
                "repositoryUri": repository_uri,
                "revisionId": revision_id,
            }],
            "invocations": [{
                "executionSuccessful": true,
                "properties": {
                    "git_slop": {
                        "schema_version": SARIF_SCHEMA_VERSION,
                        "report_schema_version": report.get("schema_version").cloned().unwrap_or(Value::Null),
                        "report_path": report_path,
                        "analyzer": report.get("analyzer").cloned().unwrap_or_else(|| json!({})),
                        "boundary_note": SARIF_BOUNDARY_NOTE,
                    },
                },
            }],
            "results": results,
            "properties": {
                "git_slop": {
                    "summary": report.get("summary").cloned().unwrap_or_else(|| json!({})),
                    "stats": report.get("stats").cloned().unwrap_or_else(|| json!({})),
                    "boundary_note": SARIF_BOUNDARY_NOTE,
                },
            },
        }],
    }))
}

pub fn render_json(payload: &Value) -> Result<String> {
    Ok(format!("{}\n", serde_json::to_string_pretty(payload)?))
}