git-slop 0.11.3

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
fn run_check(repo_root: &Path, args: CheckArgs) -> Result<i32> {
    if args.details && !(1..=10_000).contains(&args.limit) {
        return usage_error("--limit must be between 1 and 10000");
    }
    let (loaded, _) = report_or_missing(repo_root, args.report.as_deref())?;
    let incomplete_records = loaded
        .get("files")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter(|record| record.get("analysis_status").and_then(Value::as_str) != Some("analyzed"))
        .count();
    if incomplete_records > 0 && !args.allow_incomplete_evidence {
        return Err(ClassifiedError::new(
            ErrorKind::Contract,
            "incomplete_evidence",
            format!("report contains {incomplete_records} incomplete selected inventory record(s); rerun analysis with complete inputs or pass --allow-incomplete-evidence"),
        )
        .at("/files")
        .with_details(json!({"incomplete_record_count": incomplete_records}))
        .into());
    }
    let loaded_config = loaded
        .get("config")
        .cloned()
        .unwrap_or_else(config::default_config);
    let context_band = args
        .fail_on_context_band
        .map(ContextBand::as_str)
        .or_else(|| {
            loaded_config
                .pointer("/check/fail_on_context_band")
                .and_then(Value::as_str)
        })
        .unwrap_or("critical");
    let slop_band = args
        .fail_on_slop_band
        .map(SlopBand::as_str)
        .or_else(|| {
            loaded_config
                .pointer("/check/fail_on_slop_band")
                .and_then(Value::as_str)
        })
        .unwrap_or("critical");
    let failures = failing_records_in(
        &loaded,
        Some(context_band),
        Some(slop_band),
        args.include_folders,
    );
    if !matches!(args.format, CheckFormat::Text) {
        match args.format {
            CheckFormat::Json => {
                let mut payload = json!({
                    "schema_version": 1,
                    "command": "check",
                    "report": {"schema_version": loaded.get("schema_version"), "analyzer": loaded.get("analyzer"), "repo": loaded.get("repo"), "scope": loaded.get("scope")},
                    "boundary": {"context_band": context_band, "slop_band": slop_band},
                    "passed": failures.is_empty(),
                    "finding_count": failures.len(),
                    "details_included": args.details,
                    "gate_scope": if args.include_folders { "files_and_folders" } else { "files" },
                });
                if args.details {
                    let findings = failures
                        .iter()
                        .skip(args.offset)
                        .take(args.limit)
                        .cloned()
                        .collect::<Vec<_>>();
                    payload["findings"] = json!(findings);
                    payload["collection"] = json!({
                        "total": failures.len(),
                        "offset": args.offset,
                        "limit": args.limit,
                        "returned": payload["findings"].as_array().map(Vec::len).unwrap_or_default(),
                        "truncated": args.offset.saturating_add(args.limit) < failures.len(),
                    });
                }
                print_text(&render_json(&payload)?);
            }
            CheckFormat::Github => {
                for failure in &failures {
                    let path = crate::text::github_property_escape(
                        failure
                            .get("path")
                            .and_then(Value::as_str)
                            .unwrap_or_default(),
                    );
                    println!(
                        "::error file={}::Git Slop context={} slop={} score={}",
                        path,
                        failure
                            .get("context_band")
                            .and_then(Value::as_str)
                            .unwrap_or_default(),
                        failure
                            .get("slop_band")
                            .and_then(Value::as_str)
                            .unwrap_or_default(),
                        failure.get("slop_score").unwrap_or(&Value::Null)
                    );
                }
            }
            CheckFormat::Text => {}
        }
        return Ok(if failures.is_empty() { 0 } else { 1 });
    }
    if failures.is_empty() {
        println!(
            "Check passed: no file records met or exceeded context={context_band} or slop={slop_band}."
        );
        return Ok(0);
    }
    println!(
        "Check failed: {} file records met or exceeded context={context_band} or slop={slop_band}.",
        failures.len()
    );
    for failure in failures.iter().take(10) {
        println!(
            "- {} (slop={}, context={}, slop_score={})",
            safe_terminal(
                failure
                    .get("path")
                    .and_then(Value::as_str)
                    .unwrap_or_default(),
            ),
            failure
                .get("slop_band")
                .and_then(Value::as_str)
                .unwrap_or_default(),
            failure
                .get("context_band")
                .and_then(Value::as_str)
                .unwrap_or_default(),
            failure
                .get("slop_score")
                .map(ToString::to_string)
                .unwrap_or_else(|| "null".to_string()),
        );
    }
    Ok(1)
}

fn bounded_compare_output(
    payload: &Value,
    detail: CompareDetail,
    top: usize,
    offset: usize,
    limit: usize,
) -> Result<Value> {
    if limit == 0 {
        anyhow::bail!("--limit must be greater than zero");
    }
    let mut bounded = payload.clone();
    let cap = match detail {
        CompareDetail::Summary => 0,
        CompareDetail::Top => top,
        CompareDetail::Full => limit,
    };
    let start = if matches!(detail, CompareDetail::Full) {
        offset
    } else {
        0
    };
    let mut pagination = serde_json::Map::new();
    for key in [
        "file_deltas",
        "folder_deltas",
        "queue_movement",
        "overlay_deltas",
        "regressions",
    ] {
        let values = payload
            .get(key)
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default();
        let returned = values
            .iter()
            .skip(start)
            .take(cap)
            .cloned()
            .collect::<Vec<_>>();
        pagination.insert(
            key.to_string(),
            json!({
                "total": values.len(),
                "offset": start,
                "limit": cap,
                "returned": returned.len(),
                "has_more": start.saturating_add(returned.len()) < values.len()
            }),
        );
        bounded[key] = json!(returned);
    }
    bounded["detail"] = json!(match detail {
        CompareDetail::Summary => "summary",
        CompareDetail::Top => "top",
        CompareDetail::Full => "full",
    });
    bounded["pagination"] = Value::Object(pagination);
    Ok(bounded)
}

fn render_compare_ndjson(payload: &Value) -> Result<String> {
    let mut lines = vec![render_json(&json!({
        "record_type": "summary",
        "schema_version": payload.get("schema_version"),
        "stream": {
            "schema": "schemas/compare-ndjson-1.json",
            "record_types": ["summary", "file_delta", "folder_delta", "queue_movement", "overlay_delta", "regression"]
        },
        "summary": payload.get("summary"),
        "pagination": payload.get("pagination"),
        "baseline_status": payload.get("baseline_status")
    }))?];
    for (key, record_type) in [
        ("file_deltas", "file_delta"),
        ("folder_deltas", "folder_delta"),
        ("queue_movement", "queue_movement"),
        ("overlay_deltas", "overlay_delta"),
        ("regressions", "regression"),
    ] {
        for record in payload
            .get(key)
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
        {
            lines.push(render_json(
                &json!({"record_type": record_type, "record": record}),
            )?);
        }
    }
    Ok(lines.join("\n"))
}