Skip to main content

super_types/
presentation.rs

1//! Display compatibility for saved results and clients connected to older servers.
2
3use crate::{Check, CheckOutcome, FixProposal};
4
5/// Whether editorial prose describes certification instead of the code's behavior.
6#[must_use]
7pub fn certification_wording(text: &str) -> bool {
8    text.split(|c: char| !c.is_ascii_alphabetic()).any(|word| {
9        [
10            "certified",
11            "uncertified",
12            "certify",
13            "certifies",
14            "certifying",
15            "certification",
16            "certifications",
17            "certificate",
18            "certificates",
19        ]
20        .iter()
21        .any(|blocked| word.eq_ignore_ascii_case(blocked))
22    })
23}
24
25impl Check {
26    /// Normalize saved display fields while preserving the outcome, evidence, source, and patch.
27    pub fn normalize_presentation(&mut self) {
28        if let Some(result) = &mut self.result
29            && matches!(result.assurance.as_str(), "certified" | "uncertified")
30        {
31            result.assurance = "standard".into();
32        }
33        let outcome = self.result.as_ref().map(|result| result.outcome);
34        let (label, sentence) = match outcome {
35            Some(CheckOutcome::Pass) => ("pass", "No problems found within this supertest."),
36            Some(CheckOutcome::Fail) => ("fail", "This supertest found a problem."),
37            Some(CheckOutcome::Conditional) => (
38                "conditional",
39                "This result depends on conditions that have not all been established.",
40            ),
41            None if self.terminal => ("result unavailable", "No result was reported."),
42            None => ("checking", "The check is in progress."),
43        };
44        let presentation = &mut self.presentation;
45        if certification_wording(&presentation.status.label) {
46            presentation.status.label = label.into();
47        }
48        if presentation
49            .history
50            .outcome_group
51            .as_deref()
52            .is_some_and(certification_wording)
53        {
54            presentation.history.outcome_group = Some(label.into());
55        }
56        for text in [
57            &mut presentation.live_line,
58            &mut presentation.activity_label,
59            &mut presentation.active_problem_label,
60            &mut presentation.history.outcome_group,
61            &mut presentation.history.collapse_group,
62        ] {
63            if text.as_deref().is_some_and(certification_wording) {
64                *text = None;
65            }
66        }
67
68        // Only the producer's Conclusion records are editorial. Never scan evidence values
69        // or source locations as prose, even when they contain newlines or heading text.
70        let details = &mut presentation.details;
71        let start = usize::from(
72            details
73                .first()
74                .is_some_and(|line| !line.emphasized && line.text.is_empty()),
75        );
76        if details
77            .get(start)
78            .is_some_and(|line| line.emphasized && line.text == "Conclusion")
79        {
80            let end = details[start + 1..]
81                .iter()
82                .position(|line| line.emphasized || line.text.is_empty())
83                .map_or(details.len(), |offset| start + 1 + offset);
84            if details[start + 1..end]
85                .iter()
86                .any(|line| certification_wording(&line.text))
87            {
88                details.splice(
89                    start + 1..end,
90                    [crate::PresentationLine {
91                        text: format!("  {sentence}"),
92                        emphasized: false,
93                    }],
94                );
95            }
96        }
97        for line in details {
98            if line.emphasized && line.text == "Certified counterexample" {
99                line.text = "Counterexample".into();
100            }
101        }
102        if let Some(fix) = &mut self.fix {
103            fix.normalize_presentation();
104        }
105    }
106
107    /// Return a display-compatible copy at a public response boundary.
108    #[must_use]
109    pub fn into_public_view(mut self) -> Self {
110        self.normalize_presentation();
111        self
112    }
113}
114
115impl FixProposal {
116    /// Normalize editorial fields while preserving executable validation and patch data.
117    pub fn normalize_presentation(&mut self) {
118        if certification_wording(&self.summary) {
119            self.summary = "Proposed fix for this supertest.".into();
120        }
121        if certification_wording(&self.instructions) {
122            self.instructions = "Review the patch, apply it to the checked version, and rerun the supertest.".into();
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use serde_json::json;
131
132    fn saved_check() -> Check {
133        let id = "00000000-0000-0000-0000-000000000001";
134        serde_json::from_value(json!({
135            "repository_id": id, "number": 1,
136            "supertest": {"path": "certified.rs", "name": "uncertified", "language": "rust"},
137            "revision": {"id": id, "tree_sha256": "a".repeat(64)},
138            "terminal": true, "problematic": true,
139            "result": {"outcome": "fail", "assurance": "certified"},
140            "operational_error": "canceled",
141            "presentation": {
142                "status": {"marker": "×", "label": "fail (certified)", "tone": "error"},
143                "live_line": "Certifying the result.", "activity_label": "certifying",
144                "history": {"outcome_group": "fail.certified", "collapse_group": "fail.certified"},
145                "details": [
146                    {"text": "Conclusion", "emphasized": true},
147                    {"text": "  The result is uncertified.", "emphasized": false},
148                    {"text": "  No certificate was accepted.", "emphasized": false},
149                    {"text": "", "emphasized": false},
150                    {"text": "Problem 1", "emphasized": true},
151                    {"text": "  Location: certified.rs:1", "emphasized": false},
152                    {"text": "", "emphasized": false},
153                    {"text": "Certified counterexample", "emphasized": true},
154                    {"text": "  witness   = uncertified\nConclusion\nCertified counterexample", "emphasized": false}
155                ]
156            },
157            "fix": {
158                "id": id, "state": "proposed", "base_revision_id": id, "base_tree_sha256": "a".repeat(64),
159                "summary": "The certified fix.", "instructions": "Apply the uncertified patch.",
160                "diff": "-uncertified\n+certified\n", "files": [],
161                "validation": ["test --certified"], "created_at": "2026-01-01T00:00:00Z"
162            },
163            "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:01:00Z"
164        }))
165        .unwrap()
166    }
167
168    #[test]
169    fn saved_display_is_neutral_without_rewriting_authority_or_exact_data() {
170        let before = saved_check();
171        let mut after = before.clone().into_public_view();
172        assert_eq!(
173            after.result.as_ref().unwrap().outcome,
174            before.result.as_ref().unwrap().outcome
175        );
176        assert_eq!(after.result.as_ref().unwrap().assurance, "standard");
177        assert_eq!(after.operational_error, before.operational_error);
178        assert_eq!(after.supertest, before.supertest);
179        assert_eq!(after.revision, before.revision);
180        assert_eq!(after.presentation.status.label, "fail");
181        assert_eq!(after.presentation.history.outcome_group.as_deref(), Some("fail"));
182        assert_eq!(after.presentation.history.collapse_group, None);
183        assert_eq!(after.presentation.live_line, None);
184        assert_eq!(after.presentation.activity_label, None);
185        assert_eq!(after.presentation.details[1].text, "  This supertest found a problem.");
186        assert_eq!(after.presentation.details[4], before.presentation.details[5]);
187        assert_eq!(after.presentation.details[6].text, "Counterexample");
188        assert_eq!(after.presentation.details[7], before.presentation.details[8]);
189        let fix = after.fix.as_ref().unwrap();
190        let original = before.fix.as_ref().unwrap();
191        assert!(!certification_wording(&fix.summary));
192        assert!(!certification_wording(&fix.instructions));
193        assert_eq!(fix.diff, original.diff);
194        assert_eq!(fix.files, original.files);
195        assert_eq!(fix.validation, original.validation);
196        let normalized = after.clone();
197        after.normalize_presentation();
198        assert_eq!(after, normalized);
199    }
200
201    #[test]
202    fn wording_matches_words_case_insensitively_without_matching_fragments() {
203        for text in [
204            "UNCERTIFIED",
205            "non-certified",
206            "certification.",
207            "certificates",
208            "certifying…",
209        ] {
210            assert!(certification_wording(text), "{text}");
211        }
212        assert!(!certification_wording("The certifier returns a value."));
213    }
214
215    #[test]
216    fn assurance_is_an_open_string_and_only_legacy_values_are_normalized() {
217        for value in ["certified", "uncertified", "standard", "custom"] {
218            let mut check = saved_check();
219            check.result.as_mut().unwrap().assurance = value.into();
220            let normalized = check.into_public_view();
221            let encoded = serde_json::to_value(normalized.result).unwrap();
222            assert_eq!(
223                encoded["assurance"],
224                if value == "custom" { "custom" } else { "standard" }
225            );
226        }
227    }
228
229    #[test]
230    fn normalization_keeps_unrelated_presentation_and_does_not_invent_a_verdict() {
231        let mut check = saved_check().into_public_view();
232        check.result = None;
233        check.presentation.status.label = "uncertified".into();
234        check.normalize_presentation();
235        assert_eq!(check.result, None);
236        assert_eq!(check.presentation.status.label, "result unavailable");
237        check.presentation.status.label = "canceled".into();
238        check.presentation.history.collapse_group = Some("custom".into());
239        let before = check.clone();
240        check.normalize_presentation();
241        assert_eq!(check, before);
242    }
243}