git_slop/report_ops/
github.rs1use super::*;
2
3pub fn health_json_payload(report: &Value) -> Value {
4 report
5 .get("health")
6 .cloned()
7 .unwrap_or_else(|| json!({"findings": []}))
8}
9
10fn github_property_escape(value: &str) -> String {
11 value
12 .replace('%', "%25")
13 .replace('\r', "%0D")
14 .replace('\n', "%0A")
15 .replace(':', "%3A")
16 .replace(',', "%2C")
17}
18
19fn github_message_escape(value: &str) -> String {
20 value
21 .replace('%', "%25")
22 .replace('\r', "%0D")
23 .replace('\n', "%0A")
24}
25
26fn health_enforcement_enabled(report: &Value) -> bool {
27 report
28 .pointer("/health/enforcement/enabled")
29 .and_then(Value::as_bool)
30 .or_else(|| {
31 report
32 .pointer("/health/enforcement")
33 .and_then(Value::as_bool)
34 })
35 .or_else(|| report.pointer("/health/enforced").and_then(Value::as_bool))
36 .unwrap_or(false)
37}
38
39pub fn render_github_annotations(report: &Value, max_annotations: usize) -> String {
40 let enforced = health_enforcement_enabled(report);
41 let lines: Vec<String> = array_at(report, &["health", "findings"])
42 .iter()
43 .take(max_annotations)
44 .map(|finding| {
45 let command =
46 if enforced && string(finding.get("severity")).eq_ignore_ascii_case("error") {
47 "error"
48 } else {
49 "warning"
50 };
51 let path = github_property_escape(&string(finding.get("path")));
52 let title = github_property_escape(&string(finding.get("title")));
53 let mut message = string(finding.get("message"));
54 let next_command = string(finding.get("next_command"));
55 if !next_command.is_empty() {
56 if !message.is_empty() && !message.chars().last().is_some_and(char::is_whitespace) {
57 message.push(' ');
58 }
59 message.push_str("Next: ");
60 message.push_str(&next_command);
61 }
62 format!(
63 "::{command} file={path},title={title}::{}",
64 github_message_escape(&message)
65 )
66 })
67 .collect();
68 if lines.is_empty() {
69 String::new()
70 } else {
71 format!("{}\n", lines.join("\n"))
72 }
73}
74
75pub fn write_prompt_pack(
76 command: &str,
77 payload: &Value,
78 report: &Value,
79 output_dir: &Path,
80) -> Result<()> {
81 if output_dir.exists() && !output_dir.is_dir() {
82 bail!(
83 "Prompt pack path is not a directory: {}",
84 output_dir.display()
85 );
86 }
87 fs::create_dir_all(output_dir)?;
88 let context = json!({
89 "prompt_pack_version": 1,
90 "command": command,
91 "payload": payload,
92 "report_excerpt": {
93 "schema_version": report.get("schema_version").cloned().unwrap_or(Value::Null),
94 "repo": report.get("repo").cloned().unwrap_or_else(|| json!({})),
95 "summary": report.get("summary").cloned().unwrap_or_else(|| json!({})),
96 "stats": report.get("stats").cloned().unwrap_or_else(|| json!({})),
97 },
98 "boundary": "Local model output is advisory only and must not rescore detector truth or mutate code, GitHub, or report data.",
99 });
100 fs::write(output_dir.join("context.json"), render_json(&context)?)?;
101 fs::write(
102 output_dir.join("prompt.md"),
103 format!(
104 "# git-slop {command} prompt pack\n\n\
105 Use only the facts in context.json.\n\n\
106 Summarize the selected detector evidence for a maintainer. Keep hotspot\n\
107 costs separate from overlay evidence. Treat model output as advisory:\n\
108 do not rescore detector truth, claim correctness, or imply a refactor is\n\
109 mandatory. If the supplied facts are insufficient, say what is missing\n\
110 instead of inventing context.\n\n\
111 Preferred output:\n\n\
112 1. Brief maintainer summary\n\
113 2. Strongest evidence\n\
114 3. Suggested next review step\n\
115 4. Boundaries and non-claims\n"
116 ),
117 )?;
118 fs::write(
119 output_dir.join("README.md"),
120 format!(
121 "# git-slop {command} Prompt Pack\n\n\
122 This directory was generated by git-slop for local model use.\n\n\
123 Files:\n\n\
124 - `context.json`: deterministic git-slop payload plus minimal report metadata.\n\
125 - `prompt.md`: prompt text for a local model.\n\
126 - `README.md`: these usage notes.\n\n\
127 Boundary rules:\n\n\
128 - This pack is advisory only.\n\
129 - Local model output must not mutate code, GitHub, or detector truth.\n\
130 - Local model output must not rescore detector truth, including\n\
131 `slop_score`, `slop_band`,\n\
132 `context_band`, or `git slop check` semantics.\n\
133 - Keep hotspot cost separate from overlay evidence.\n"
134 ),
135 )?;
136 Ok(())
137}