1use crate::audit_remediation::audit_remediation_items;
2use crate::contracts::REPORT_ARTIFACT;
3use crate::evidence_repair::{evidence_repair_queues, push_evidence_repair_queue_json_fields};
4use crate::json::{
5 option_json, push_json_artifact_header, push_json_artifact_source_context,
6 push_json_status_fields, render_match_outcome_json_compact,
7};
8use crate::{
9 DiffReport, REPORT_COMMAND_DIFF, REPORT_COMMANDS, ReportContext, ReviewSignals, Summary,
10 render_count_fields_with_policy_context,
11};
12use allow_core::{Finding, MatchOutcome, MatchStatus, json_escape, normalize_path};
13
14pub fn render_json(
15 command: &str,
16 findings: &[Finding],
17 outcomes: &[MatchOutcome],
18 failed: bool,
19) -> String {
20 render_json_with_context(
21 command,
22 findings,
23 outcomes,
24 failed,
25 ReportContext::default(),
26 )
27}
28
29pub fn render_json_with_context(
30 command: &str,
31 findings: &[Finding],
32 outcomes: &[MatchOutcome],
33 failed: bool,
34 context: ReportContext<'_>,
35) -> String {
36 render_json_report(command, findings, outcomes, failed, context, None)
37}
38
39pub fn render_json_with_context_and_diff(
40 command: &str,
41 findings: &[Finding],
42 outcomes: &[MatchOutcome],
43 failed: bool,
44 context: ReportContext<'_>,
45 diff: DiffReport<'_>,
46) -> String {
47 assert_eq!(
48 command, REPORT_COMMAND_DIFF,
49 "diff report artifacts support only diff command"
50 );
51 render_json_report(command, findings, outcomes, failed, context, Some(diff))
52}
53
54fn render_json_report(
55 command: &str,
56 findings: &[Finding],
57 outcomes: &[MatchOutcome],
58 failed: bool,
59 context: ReportContext<'_>,
60 diff: Option<DiffReport<'_>>,
61) -> String {
62 assert!(
63 REPORT_COMMANDS.contains(&command),
64 "report artifacts support only audit, check, or diff commands"
65 );
66 let summary = Summary::from_outcomes(outcomes);
67 let mut out = String::new();
68 out.push_str("{\n");
69 push_json_artifact_header(&mut out, REPORT_ARTIFACT, command);
70 push_json_status_fields(&mut out, failed);
71 push_json_artifact_source_context(&mut out, context.into());
72 out.push_str(" \"summary\": {\n");
73 out.push_str(&format!(" \"findings\": {},\n", findings.len()));
74 out.push_str(&format!(" \"outcomes\": {},\n", summary.total));
75 out.push_str(&render_count_fields_with_policy_context(
76 &summary,
77 context.baseline_debt_entries,
78 context.policy_missing_evidence_entries,
79 context.broken_evidence_links,
80 context.weak_evidence_references,
81 " ",
82 ));
83 out.push_str(" },\n");
84 out.push_str(" \"trend\": {\n");
85 out.push_str(&render_trend_fields(&summary, context, " "));
86 out.push_str(" },\n");
87 append_audit_remediation_roadmap_json(command, &summary, context, &mut out);
88 append_evidence_repair_queues_json(&summary, context, &mut out);
89 if let Some(source_inventory) = crate::render_source_inventory_json(findings, outcomes, " ") {
90 out.push_str(" \"source_inventory\": ");
91 out.push_str(&source_inventory);
92 out.push_str(",\n");
93 }
94 out.push_str(" \"outcomes\": [\n");
95 for (i, outcome) in outcomes.iter().enumerate() {
96 if i > 0 {
97 out.push_str(",\n");
98 }
99 out.push_str(" ");
100 out.push_str(&render_match_outcome_json_compact(outcome));
101 }
102 out.push_str("\n ],\n");
103 out.push_str(" \"findings\": [\n");
104 for (i, finding) in findings.iter().enumerate() {
105 if i > 0 {
106 out.push_str(",\n");
107 }
108 out.push_str(" {");
109 out.push_str(&format!("\"kind\": \"{}\", ", finding.kind.as_str()));
110 out.push_str(&format!(
111 "\"family\": {}, ",
112 option_json(finding.family.as_deref())
113 ));
114 out.push_str(&format!(
115 "\"path\": \"{}\", ",
116 json_escape(&normalize_path(&finding.path))
117 ));
118 out.push_str(&format!(
119 "\"line\": {}, ",
120 finding
121 .span
122 .as_ref()
123 .map(|s| s.line.to_string())
124 .unwrap_or_else(|| "null".to_string())
125 ));
126 out.push_str(&format!(
127 "\"container\": {}, ",
128 option_json(finding.identity.container.as_deref())
129 ));
130 out.push_str(&format!(
131 "\"source_package\": {}, ",
132 option_json(finding.identity.crate_name.as_deref())
133 ));
134 out.push_str(&format!(
135 "\"ast_kind\": \"{}\"",
136 json_escape(&finding.identity.ast_kind)
137 ));
138 out.push('}');
139 }
140 match diff {
141 Some(diff) => {
142 out.push_str("\n ],\n");
143 out.push_str(" \"diff\": ");
144 out.push_str(
145 &crate::diff_json::render_diff_posture_json_with_evidence_health(
146 diff,
147 context.broken_evidence_links.unwrap_or(0),
148 context
149 .policy_missing_evidence_entries
150 .unwrap_or(0)
151 .max(summary.count(MatchStatus::EvidenceMissing)),
152 context.weak_evidence_references.unwrap_or(0),
153 ),
154 );
155 out.push_str("\n}\n");
156 }
157 None => out.push_str("\n ]\n}"),
158 }
159 out
160}
161
162fn append_evidence_repair_queues_json(
163 summary: &Summary,
164 context: ReportContext<'_>,
165 out: &mut String,
166) {
167 let queues = evidence_repair_queues(summary, ReviewSignals::from_summary(summary, context));
168 if queues.is_empty() {
169 return;
170 }
171
172 out.push_str(" \"evidence_repair_queues\": [\n");
173 for (index, queue) in queues.iter().enumerate() {
174 if index > 0 {
175 out.push_str(",\n");
176 }
177 out.push_str(" {\n");
178 push_evidence_repair_queue_json_fields(out, queue, " ");
179 out.push_str(" }");
180 }
181 out.push_str("\n ],\n");
182}
183
184fn append_audit_remediation_roadmap_json(
185 command: &str,
186 summary: &Summary,
187 context: ReportContext<'_>,
188 out: &mut String,
189) {
190 if command != "audit" {
191 return;
192 }
193 let roadmap = audit_remediation_items(summary, ReviewSignals::from_summary(summary, context));
194 if roadmap.is_empty() {
195 return;
196 }
197
198 out.push_str(" \"audit_remediation_roadmap\": [\n");
199 for (index, item) in roadmap.iter().enumerate() {
200 if index > 0 {
201 out.push_str(",\n");
202 }
203 out.push_str(" {\n");
204 out.push_str(&format!(
205 " \"signal\": \"{}\",\n",
206 json_escape(item.signal)
207 ));
208 out.push_str(&format!(
209 " \"label\": \"{}\",\n",
210 json_escape(item.label)
211 ));
212 out.push_str(&format!(
213 " \"route_kind\": \"{}\",\n",
214 json_escape(item.route.route_kind)
215 ));
216 if let Some(item_kind) = item.route.item_kind {
217 out.push_str(&format!(
218 " \"item_kind\": \"{}\",\n",
219 json_escape(item_kind)
220 ));
221 }
222 if let Some(worklist_status) = item.route.worklist_status {
223 out.push_str(&format!(
224 " \"worklist_status\": \"{}\",\n",
225 json_escape(worklist_status)
226 ));
227 }
228 if let Some(worklist_filter) = item.route.worklist_filter {
229 out.push_str(&format!(
230 " \"worklist_filter\": \"{}\",\n",
231 json_escape(worklist_filter)
232 ));
233 }
234 out.push_str(&format!(" \"count\": {},\n", item.count));
235 out.push_str(&format!(
236 " \"command\": \"{}\"\n",
237 json_escape(item.command)
238 ));
239 out.push_str(" }");
240 }
241 out.push_str("\n ],\n");
242}
243
244fn render_trend_fields(summary: &Summary, context: ReportContext<'_>, indent: &str) -> String {
245 let signals = ReviewSignals::from_summary(summary, context);
246 let mut fields = vec![
247 ("review_items", signals.review_items),
248 ("new", summary.count(MatchStatus::New)),
249 ("expired", summary.count(MatchStatus::Expired)),
250 ("review_due", summary.count(MatchStatus::ReviewDue)),
251 ("stale", summary.count(MatchStatus::Stale)),
252 ("ambiguous", summary.count(MatchStatus::Ambiguous)),
253 (
254 "invalid_selector",
255 summary.count(MatchStatus::InvalidSelector),
256 ),
257 (
258 "missing_required_field",
259 summary.count(MatchStatus::MissingRequiredField),
260 ),
261 (
262 "evidence_missing",
263 summary.count(MatchStatus::EvidenceMissing),
264 ),
265 ("baseline_debt", signals.baseline_debt),
266 ];
267 if signals.policy_missing_evidence > summary.count(MatchStatus::EvidenceMissing) {
268 fields.push(("policy_missing_evidence", signals.policy_missing_evidence));
269 }
270 if signals.broken_evidence_links > 0 {
271 fields.push(("broken_evidence_links", signals.broken_evidence_links));
272 }
273 if signals.weak_evidence_references > 0 {
274 fields.push(("weak_evidence_references", signals.weak_evidence_references));
275 }
276 fields
277 .iter()
278 .enumerate()
279 .map(|(idx, (name, value))| {
280 let comma = if idx + 1 == fields.len() { "" } else { "," };
281 format!("{indent}\"{name}\": {value}{comma}\n")
282 })
283 .collect()
284}