git-slop 0.9.0

Deterministic repository health and maintenance-pressure analysis for humans and AI agents.
Documentation
use super::*;

pub fn health_json_payload(report: &Value) -> Value {
    report
        .get("health")
        .cloned()
        .unwrap_or_else(|| json!({"findings": []}))
}

fn github_property_escape(value: &str) -> String {
    value
        .replace('%', "%25")
        .replace('\r', "%0D")
        .replace('\n', "%0A")
        .replace(':', "%3A")
        .replace(',', "%2C")
}

fn github_message_escape(value: &str) -> String {
    value
        .replace('%', "%25")
        .replace('\r', "%0D")
        .replace('\n', "%0A")
}

fn health_enforcement_enabled(report: &Value) -> bool {
    report
        .pointer("/health/enforcement/enabled")
        .and_then(Value::as_bool)
        .or_else(|| {
            report
                .pointer("/health/enforcement")
                .and_then(Value::as_bool)
        })
        .or_else(|| report.pointer("/health/enforced").and_then(Value::as_bool))
        .unwrap_or(false)
}

pub fn render_github_annotations(report: &Value, max_annotations: usize) -> String {
    let enforced = health_enforcement_enabled(report);
    let lines: Vec<String> = array_at(report, &["health", "findings"])
        .iter()
        .take(max_annotations)
        .map(|finding| {
            let command =
                if enforced && string(finding.get("severity")).eq_ignore_ascii_case("error") {
                    "error"
                } else {
                    "warning"
                };
            let path = github_property_escape(&string(finding.get("path")));
            let title = github_property_escape(&string(finding.get("title")));
            let mut message = string(finding.get("message"));
            let next_command = string(finding.get("next_command"));
            if !next_command.is_empty() {
                if !message.is_empty() && !message.chars().last().is_some_and(char::is_whitespace) {
                    message.push(' ');
                }
                message.push_str("Next: ");
                message.push_str(&next_command);
            }
            format!(
                "::{command} file={path},title={title}::{}",
                github_message_escape(&message)
            )
        })
        .collect();
    if lines.is_empty() {
        String::new()
    } else {
        format!("{}\n", lines.join("\n"))
    }
}

pub fn write_prompt_pack(
    command: &str,
    payload: &Value,
    report: &Value,
    output_dir: &Path,
) -> Result<()> {
    if output_dir.exists() && !output_dir.is_dir() {
        bail!(
            "Prompt pack path is not a directory: {}",
            output_dir.display()
        );
    }
    fs::create_dir_all(output_dir)?;
    let context = json!({
        "prompt_pack_version": 1,
        "command": command,
        "payload": payload,
        "report_excerpt": {
            "schema_version": report.get("schema_version").cloned().unwrap_or(Value::Null),
            "repo": report.get("repo").cloned().unwrap_or_else(|| json!({})),
            "summary": report.get("summary").cloned().unwrap_or_else(|| json!({})),
            "stats": report.get("stats").cloned().unwrap_or_else(|| json!({})),
        },
        "boundary": "Local model output is advisory only and must not rescore detector truth or mutate code, GitHub, or report data.",
    });
    fs::write(output_dir.join("context.json"), render_json(&context)?)?;
    fs::write(
        output_dir.join("prompt.md"),
        format!(
            "# git-slop {command} prompt pack\n\n\
             Use only the facts in context.json.\n\n\
             Summarize the selected detector evidence for a maintainer. Keep hotspot\n\
             costs separate from overlay evidence. Treat model output as advisory:\n\
             do not rescore detector truth, claim correctness, or imply a refactor is\n\
             mandatory. If the supplied facts are insufficient, say what is missing\n\
             instead of inventing context.\n\n\
             Preferred output:\n\n\
             1. Brief maintainer summary\n\
             2. Strongest evidence\n\
             3. Suggested next review step\n\
             4. Boundaries and non-claims\n"
        ),
    )?;
    fs::write(
        output_dir.join("README.md"),
        format!(
            "# git-slop {command} Prompt Pack\n\n\
             This directory was generated by git-slop for local model use.\n\n\
             Files:\n\n\
             - `context.json`: deterministic git-slop payload plus minimal report metadata.\n\
             - `prompt.md`: prompt text for a local model.\n\
             - `README.md`: these usage notes.\n\n\
             Boundary rules:\n\n\
             - This pack is advisory only.\n\
             - Local model output must not mutate code, GitHub, or detector truth.\n\
             - Local model output must not rescore detector truth, including\n\
               `slop_score`, `slop_band`,\n\
               `context_band`, or `git slop check` semantics.\n\
             - Keep hotspot cost separate from overlay evidence.\n"
        ),
    )?;
    Ok(())
}