Skip to main content

agi4_report/
lib.rs

1//! Markdown report rendering for AGI/4 verdicts.
2//!
3//! Converts VerdictOutput JSON into human-readable Markdown with
4//! provenance links, evidence tables, and per-conjunct sections.
5
6use agi4_schema::{ConjunctReport, VerdictOutput};
7
8/// Render a verdict as Markdown.
9///
10/// Produces a complete human-readable report including:
11/// - Verdict summary with model metadata
12/// - Per-conjunct evaluation with evidence tables
13/// - Margin analysis and consistency check results
14/// - Known gaps acknowledgments
15pub 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}