1use agi4_schema::{ConjunctReport, VerdictOutput};
7
8pub fn render(verdict: &VerdictOutput) -> String {
16 let mut output = String::new();
17
18 output.push_str("# AGI/4 Attestation Verdict\n\n");
19
20 render_metadata(&mut output, verdict);
21 render_conjuncts(&mut output, verdict);
22 render_consistency_check(&mut output, verdict);
23 render_verdict_summary(&mut output, verdict);
24 render_known_gaps(&mut output, verdict);
25
26 output
27}
28
29fn render_metadata(output: &mut String, verdict: &VerdictOutput) {
30 output.push_str("## Evaluation Metadata\n\n");
31 output.push_str(&format!("**Model:** {}\n", verdict.model.id));
32
33 if let Some(provider) = &verdict.model.provider {
34 output.push_str(&format!("**Provider:** {}\n", provider));
35 }
36
37 if let Some(version) = &verdict.model.version_or_date {
38 output.push_str(&format!("**Version/Date:** {}\n", version));
39 }
40
41 output.push_str(&format!(
42 "**Specification Version:** {}\n",
43 verdict.spec_version
44 ));
45 output.push_str(&format!("**Runner Version:** {}\n", verdict.runner_version));
46 output.push_str(&format!("**Run Timestamp:** {}\n", verdict.run_timestamp));
47 output.push('\n');
48}
49
50fn render_conjuncts(output: &mut String, verdict: &VerdictOutput) {
51 output.push_str("## Per-Conjunct Evaluation\n\n");
52
53 output.push_str(&render_conjunct_section(
54 "Generality",
55 &verdict.conjuncts.generality,
56 ));
57 output.push_str(&render_conjunct_section(
58 "Economic Substitutability",
59 &verdict.conjuncts.economic_substitutability,
60 ));
61 output.push_str(&render_conjunct_section(
62 "Environmental Transfer",
63 &verdict.conjuncts.environmental_transfer,
64 ));
65 output.push_str(&render_conjunct_section(
66 "Autonomous Agency",
67 &verdict.conjuncts.autonomous_agency,
68 ));
69}
70
71fn render_conjunct_section(name: &str, conjunct: &ConjunctReport) -> String {
72 let mut section = String::new();
73
74 section.push_str(&format!("### {}\n\n", name));
75 section.push_str(&format!("**Status:** `{}`\n\n", conjunct.status));
76
77 if !conjunct.evidence.is_empty() {
78 section.push_str("#### Evidence\n\n");
79 section.push_str("| Source | Measurement | Value | Threshold | Passes |\n");
80 section.push_str("|--------|-------------|-------|-----------|--------|\n");
81
82 for evidence in &conjunct.evidence {
83 let passes = evidence
84 .passes_threshold
85 .map(|p| if p { "✓" } else { "✗" })
86 .unwrap_or("—");
87 let threshold = evidence
88 .threshold
89 .map(|t| format!("{:.2}", t))
90 .unwrap_or_else(|| "—".to_string());
91
92 section.push_str(&format!(
93 "| {} | {} | {} | {} | {} |\n",
94 evidence.source, evidence.measurement, evidence.value, threshold, passes
95 ));
96 }
97
98 section.push('\n');
99
100 section.push_str("#### Evidence Provenance\n\n");
101 for evidence in &conjunct.evidence {
102 section.push_str(&format!(
103 "**{}:** [{}]({})\n",
104 evidence.source, evidence.measurement, evidence.provenance.source_url
105 ));
106 }
107 section.push('\n');
108 }
109
110 if let Some(margins) = &conjunct.margins {
111 section.push_str(&format!(
112 "#### Margins\n\n- **Min:** {:.2}\n- **Max:** {:.2}\n\n",
113 margins.min, margins.max
114 ));
115 }
116
117 section
118}
119
120fn render_consistency_check(output: &mut String, verdict: &VerdictOutput) {
121 output.push_str("## Consistency Check\n\n");
122 output.push_str(&format!(
123 "**Status:** `{}`\n\n",
124 verdict.consistency_check.status
125 ));
126
127 if !verdict.consistency_check.failed_rules.is_empty() {
128 output.push_str("**Failed Rules:**\n\n");
129 for rule in &verdict.consistency_check.failed_rules {
130 output.push_str(&format!("- {}\n", rule));
131 }
132 output.push('\n');
133 }
134
135 if let Some(detail) = &verdict.consistency_check.detail {
136 output.push_str(&format!("**Detail:** {}\n\n", detail));
137 }
138}
139
140fn render_verdict_summary(output: &mut String, verdict: &VerdictOutput) {
141 output.push_str("## Verdict\n\n");
142 output.push_str(&format!(
143 "**Result:** `{}`\n\n",
144 verdict.verdict.to_uppercase()
145 ));
146
147 if !verdict.verdict_reasons.is_empty() {
148 output.push_str("**Reasons:**\n\n");
149 for reason in &verdict.verdict_reasons {
150 output.push_str(&format!("- {}\n", reason));
151 }
152 output.push('\n');
153 }
154}
155
156fn render_known_gaps(output: &mut String, verdict: &VerdictOutput) {
157 if !verdict.known_gaps_acknowledged.is_empty() {
158 output.push_str("## Known Gaps Acknowledged\n\n");
159 for gap in &verdict.known_gaps_acknowledged {
160 output.push_str(&format!("- {}\n", gap));
161 }
162 output.push('\n');
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use agi4_schema::{
170 ConjunctReport, ConjunctsOutput, ConsistencyCheckOutput, ModelMetadata, VerdictOutput,
171 };
172
173 fn create_test_verdict() -> VerdictOutput {
174 VerdictOutput {
175 spec_version: "0.1.0".to_string(),
176 runner_version: "0.1.0".to_string(),
177 run_timestamp: "2026-05-26T00:00:00Z".to_string(),
178 model: ModelMetadata {
179 id: "test-model".to_string(),
180 provider: Some("test-lab".to_string()),
181 version_or_date: Some("2026-05-26".to_string()),
182 },
183 conjuncts: ConjunctsOutput {
184 generality: ConjunctReport {
185 status: "pass".to_string(),
186 evidence: vec![],
187 margins: None,
188 },
189 economic_substitutability: ConjunctReport {
190 status: "pass".to_string(),
191 evidence: vec![],
192 margins: None,
193 },
194 environmental_transfer: ConjunctReport {
195 status: "partial".to_string(),
196 evidence: vec![],
197 margins: None,
198 },
199 autonomous_agency: ConjunctReport {
200 status: "pass".to_string(),
201 evidence: vec![],
202 margins: None,
203 },
204 },
205 consistency_check: ConsistencyCheckOutput {
206 status: "pass".to_string(),
207 failed_rules: vec![],
208 detail: None,
209 },
210 verdict: "not_attested".to_string(),
211 verdict_reasons: vec!["environmental_transfer".to_string()],
212 known_gaps_acknowledged: vec!["nes_underspecified".to_string()],
213 }
214 }
215
216 #[test]
217 fn render_produces_markdown() {
218 let verdict = create_test_verdict();
219 let markdown = render(&verdict);
220
221 assert!(!markdown.is_empty());
222 assert!(markdown.contains("# AGI/4 Attestation Verdict"));
223 assert!(markdown.contains("test-model"));
224 }
225
226 #[test]
227 fn render_includes_metadata() {
228 let verdict = create_test_verdict();
229 let markdown = render(&verdict);
230
231 assert!(markdown.contains("## Evaluation Metadata"));
232 assert!(markdown.contains("**Model:** test-model"));
233 assert!(markdown.contains("**Provider:** test-lab"));
234 assert!(markdown.contains("**Version/Date:** 2026-05-26"));
235 assert!(markdown.contains("**Specification Version:** 0.1.0"));
236 assert!(markdown.contains("**Runner Version:** 0.1.0"));
237 assert!(markdown.contains("**Run Timestamp:** 2026-05-26T00:00:00Z"));
238 }
239
240 #[test]
241 fn render_includes_conjuncts() {
242 let verdict = create_test_verdict();
243 let markdown = render(&verdict);
244
245 assert!(markdown.contains("## Per-Conjunct Evaluation"));
246 assert!(markdown.contains("### Generality"));
247 assert!(markdown.contains("### Economic Substitutability"));
248 assert!(markdown.contains("### Environmental Transfer"));
249 assert!(markdown.contains("### Autonomous Agency"));
250 }
251
252 #[test]
253 fn render_includes_conjunct_status() {
254 let verdict = create_test_verdict();
255 let markdown = render(&verdict);
256
257 assert!(markdown.contains("`pass`"));
258 assert!(markdown.contains("`partial`"));
259 }
260
261 #[test]
262 fn render_includes_consistency_check() {
263 let verdict = create_test_verdict();
264 let markdown = render(&verdict);
265
266 assert!(markdown.contains("## Consistency Check"));
267 assert!(markdown.contains("**Status:**"));
268 }
269
270 #[test]
271 fn render_includes_verdict_summary() {
272 let verdict = create_test_verdict();
273 let markdown = render(&verdict);
274
275 assert!(markdown.contains("## Verdict"));
276 assert!(markdown.contains("**Result:**"));
277 assert!(markdown.contains("NOT_ATTESTED"));
278 }
279
280 #[test]
281 fn render_includes_verdict_reasons() {
282 let verdict = create_test_verdict();
283 let markdown = render(&verdict);
284
285 assert!(markdown.contains("**Reasons:**"));
286 assert!(markdown.contains("environmental_transfer"));
287 }
288
289 #[test]
290 fn render_includes_known_gaps() {
291 let verdict = create_test_verdict();
292 let markdown = render(&verdict);
293
294 assert!(markdown.contains("## Known Gaps Acknowledged"));
295 assert!(markdown.contains("nes_underspecified"));
296 }
297
298 #[test]
299 fn render_snapshot_test() {
300 let verdict = create_test_verdict();
301 let markdown = render(&verdict);
302
303 let expected = r#"# AGI/4 Attestation Verdict
304
305## Evaluation Metadata
306
307**Model:** test-model
308**Provider:** test-lab
309**Version/Date:** 2026-05-26
310**Specification Version:** 0.1.0
311**Runner Version:** 0.1.0
312**Run Timestamp:** 2026-05-26T00:00:00Z
313
314## Per-Conjunct Evaluation
315
316### Generality
317
318**Status:** `pass`
319
320### Economic Substitutability
321
322**Status:** `pass`
323
324### Environmental Transfer
325
326**Status:** `partial`
327
328### Autonomous Agency
329
330**Status:** `pass`
331
332## Consistency Check
333
334**Status:** `pass`
335
336## Verdict
337
338**Result:** `NOT_ATTESTED`
339
340**Reasons:**
341
342- environmental_transfer
343
344## Known Gaps Acknowledged
345
346- nes_underspecified
347
348"#;
349
350 assert_eq!(markdown.trim(), expected.trim());
351 }
352}