1use crate::contracts::RECEIPT_ARTIFACT;
2use crate::evidence_repair::{
3 evidence_repair_queues_from_context, push_evidence_repair_queue_json_fields,
4};
5use crate::json::{
6 push_json_artifact_header, push_json_artifact_source_context, push_json_receipt_run_metadata,
7 push_json_status_fields, push_json_status_fields_with_status,
8};
9use crate::{
10 ARTIFACT_STATUS_ERROR, RECEIPT_COMMAND_CHECK, ReportContext, Summary, baseline_debt_count,
11 render_count_fields_with_policy_context, render_source_inventory_json,
12};
13use allow_core::{Finding, MatchOutcome, json_escape};
14
15pub fn render_receipt(command: &str, outcomes: &[MatchOutcome], failed: bool) -> String {
16 render_receipt_with_context(command, outcomes, failed, ReportContext::default())
17}
18
19pub fn render_receipt_with_context(
20 command: &str,
21 outcomes: &[MatchOutcome],
22 failed: bool,
23 context: ReportContext<'_>,
24) -> String {
25 render_receipt_json(command, None, outcomes, failed, context)
26}
27
28pub fn render_receipt_with_context_and_inventory(
29 command: &str,
30 findings: &[Finding],
31 outcomes: &[MatchOutcome],
32 failed: bool,
33 context: ReportContext<'_>,
34) -> String {
35 render_receipt_json(command, Some(findings), outcomes, failed, context)
36}
37
38pub fn render_error_receipt(diagnostic: &str, context: ReportContext<'_>) -> String {
39 let mut out = String::new();
40 out.push_str("{\n");
41 push_json_artifact_header(&mut out, RECEIPT_ARTIFACT, RECEIPT_COMMAND_CHECK);
42 push_json_status_fields_with_status(&mut out, ARTIFACT_STATUS_ERROR, true);
43 push_json_receipt_run_metadata(&mut out, context);
44 out.push_str(&format!(
45 " \"diagnostic\": \"{}\",\n",
46 json_escape(diagnostic)
47 ));
48 push_json_artifact_source_context(&mut out, context.into());
49 out.push_str(" \"counts\": {\n");
50 out.push_str(&render_count_fields_with_policy_context(
51 &Summary::default(),
52 None,
53 None,
54 None,
55 None,
56 " ",
57 ));
58 out.push_str(" }\n");
59 out.push_str("}\n");
60 out
61}
62
63fn render_receipt_json(
64 command: &str,
65 findings: Option<&[Finding]>,
66 outcomes: &[MatchOutcome],
67 failed: bool,
68 context: ReportContext<'_>,
69) -> String {
70 assert_eq!(
71 command, RECEIPT_COMMAND_CHECK,
72 "receipt artifacts support only the check command"
73 );
74 let summary = Summary::from_outcomes(outcomes);
75 let mut out = String::new();
76 out.push_str("{\n");
77 push_json_artifact_header(&mut out, RECEIPT_ARTIFACT, command);
78 push_json_status_fields(&mut out, failed);
79 push_json_receipt_run_metadata(&mut out, context);
80 push_json_artifact_source_context(&mut out, context.into());
81 out.push_str(" \"counts\": {\n");
82 out.push_str(&render_count_fields_with_policy_context(
83 &summary,
84 Some(baseline_debt_count(&summary, context)),
85 context.policy_missing_evidence_entries,
86 context.broken_evidence_links,
87 context.weak_evidence_references,
88 " ",
89 ));
90 out.push_str(" }");
91 append_evidence_repair_queues_json(&summary, context, &mut out);
92 if let Some(source_inventory) =
93 findings.and_then(|findings| render_source_inventory_json(findings, outcomes, " "))
94 {
95 out.push_str(",\n \"source_inventory\": ");
96 out.push_str(&source_inventory);
97 out.push('\n');
98 } else {
99 out.push('\n');
100 }
101 out.push_str("}\n");
102 out
103}
104
105fn append_evidence_repair_queues_json(
106 summary: &Summary,
107 context: ReportContext<'_>,
108 out: &mut String,
109) {
110 let queues = evidence_repair_queues_from_context(summary, context);
111 if queues.is_empty() {
112 return;
113 }
114
115 out.push_str(",\n \"evidence_repair_queues\": [\n");
116 for (index, queue) in queues.iter().enumerate() {
117 if index > 0 {
118 out.push_str(",\n");
119 }
120 out.push_str(" {\n");
121 push_evidence_repair_queue_json_fields(out, queue, " ");
122 out.push_str(" }");
123 }
124 out.push_str("\n ]");
125}