Skip to main content

llm_assisted_api_debugging_lab/
prose.rs

1//! Per-rule prose loader.
2//!
3//! All editorial content (likely-cause templates, hypotheses, unknowns,
4//! next-step bullets, escalation-note copy, severity rationale) lives in
5//! `prose.toml` at the crate root, embedded into the binary via
6//! `include_str!` and parsed once on first access through `OnceLock`.
7//!
8//! Rule logic in `diagnose.rs` references prose by rule name; mismatches
9//! between the two surface as a panic at first access (during tests,
10//! immediately on first use). This is the right severity: a missing prose
11//! key or malformed prose.toml is a programming error, not a runtime
12//! condition — and the test suite exercises every rule, so any drift fails
13//! `cargo test` rather than reaching production. The `clippy::panic`
14//! allow below is scoped to this load-time validation surface only.
15
16#![allow(clippy::panic)]
17
18use serde::Deserialize;
19use std::collections::BTreeMap;
20use std::sync::OnceLock;
21
22const PROSE_TOML: &str = include_str!("../prose.toml");
23
24/// Top-level deserialized form of `prose.toml`.
25///
26/// The TOML file looks like `[rules.<rule_name>] ...`, which deserializes
27/// as a single `rules` map keyed on rule name. `BTreeMap` rather than
28/// `HashMap` so iteration order during validation tests is stable.
29#[derive(Debug, Deserialize)]
30pub struct Prose {
31    rules: BTreeMap<String, RuleProse>,
32}
33
34/// Per-rule prose entry.
35///
36/// `likely_cause` and the three `likely_cause_template_*` fields are all
37/// `Option`; each rule arm uses exactly one of them depending on whether
38/// it needs to interpolate a placeholder. The
39/// `every_rule_template_kind_matches_its_call_site` test pins which
40/// variant each rule is expected to populate.
41#[derive(Debug, Deserialize)]
42pub struct RuleProse {
43    pub severity_rationale: String,
44    #[serde(default)]
45    pub likely_cause: Option<String>,
46    #[serde(default)]
47    pub likely_cause_template: Option<String>,
48    #[serde(default)]
49    pub likely_cause_template_with_field: Option<String>,
50    #[serde(default)]
51    pub likely_cause_template_no_field: Option<String>,
52    pub hypotheses: Vec<String>,
53    pub unknowns: Vec<String>,
54    pub next_steps: Vec<String>,
55    pub escalation_note: String,
56}
57
58impl Prose {
59    pub fn rule(&self, name: &str) -> &RuleProse {
60        self.rules
61            .get(name)
62            .unwrap_or_else(|| panic!("missing prose entry for rule `{name}` in prose.toml"))
63    }
64}
65
66impl RuleProse {
67    /// Static likely-cause text for rules that don't interpolate fields.
68    pub fn likely_cause_static(&self) -> &str {
69        self.likely_cause.as_deref().unwrap_or_else(|| {
70            panic!(
71                "rule prose has no static `likely_cause`; \
72                 use a `likely_cause_template*` accessor instead"
73            )
74        })
75    }
76
77    /// Likely-cause text with `{host}` substituted.
78    pub fn likely_cause_with_host(&self, host: &str) -> String {
79        self.likely_cause_template
80            .as_deref()
81            .unwrap_or_else(|| panic!("rule prose has no `likely_cause_template`"))
82            .replace("{host}", host)
83    }
84
85    /// Likely-cause text with `{peer}` substituted.
86    pub fn likely_cause_with_peer(&self, peer: &str) -> String {
87        self.likely_cause_template
88            .as_deref()
89            .unwrap_or_else(|| panic!("rule prose has no `likely_cause_template`"))
90            .replace("{peer}", peer)
91    }
92
93    /// Likely-cause text for `bad_payload`-style rules where the validated
94    /// field may or may not be present. Picks the right template and
95    /// substitutes `{field}` when applicable.
96    pub fn likely_cause_with_optional_field(&self, field: Option<&str>) -> String {
97        match field {
98            Some(f) => self
99                .likely_cause_template_with_field
100                .as_deref()
101                .unwrap_or_else(|| panic!("rule prose has no `likely_cause_template_with_field`"))
102                .replace("{field}", f),
103            None => self
104                .likely_cause_template_no_field
105                .as_deref()
106                .unwrap_or_else(|| panic!("rule prose has no `likely_cause_template_no_field`"))
107                .to_string(),
108        }
109    }
110}
111
112/// Access the parsed prose. Loaded and validated once on first call.
113pub fn prose() -> &'static Prose {
114    static CACHE: OnceLock<Prose> = OnceLock::new();
115    CACHE.get_or_init(|| {
116        toml::from_str(PROSE_TOML).unwrap_or_else(|e| panic!("prose.toml is malformed: {e}"))
117    })
118}
119
120#[cfg(test)]
121mod tests {
122    #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
123    use super::*;
124
125    #[test]
126    fn prose_parses() {
127        // Side effect: panics if the embedded TOML is invalid.
128        let _ = prose();
129    }
130
131    /// Every likely-cause template variant a rule arm can call.
132    ///
133    /// This enum exists only inside the test below; it pairs each
134    /// `RuleProse` accessor with the rule arms that use it, so the test
135    /// can assert that the prose entry actually populates the right
136    /// template fields.
137    enum TemplateKind {
138        Static,
139        Host,
140        Peer,
141        OptionalField,
142    }
143
144    /// Source of truth for the rule-name ↔ template-kind binding.
145    /// Mirrors the call sites in `diagnose.rs`. If a rule arm starts
146    /// calling a different accessor (e.g. switches from `static` to
147    /// `with_host`), the binding here must change to match — and the
148    /// test below will catch a missing template field at test time
149    /// rather than at first runtime call.
150    const TEMPLATE_BINDINGS: &[(&str, TemplateKind)] = &[
151        ("dns_failure", TemplateKind::Host),
152        ("tls_failure", TemplateKind::Peer),
153        ("connection_timeout", TemplateKind::Static),
154        ("webhook_signature", TemplateKind::Static),
155        ("rate_limit", TemplateKind::Static),
156        ("auth_missing", TemplateKind::Static),
157        ("bad_payload", TemplateKind::OptionalField),
158        ("unknown", TemplateKind::Static),
159    ];
160
161    #[test]
162    fn every_rule_named_in_diagnose_has_prose() {
163        let p = prose();
164        for (rule, _) in TEMPLATE_BINDINGS {
165            // Triggers the panic in `Prose::rule` if missing.
166            let _ = p.rule(rule);
167        }
168    }
169
170    /// Exercise every rule's likely-cause accessor in the same shape its
171    /// call site uses. Closes the gap that
172    /// `every_rule_named_in_diagnose_has_prose` leaves: a rule whose
173    /// `prose.toml` entry has, say, `likely_cause` set when the call
174    /// site asks for `likely_cause_template_with_field` would still pass
175    /// the presence test but panic at first runtime call. This test
176    /// surfaces that mismatch as a test failure instead.
177    ///
178    /// The placeholder values fed in here (`example.test`,
179    /// `field_name`) are chosen to be obviously fake; we only care that
180    /// the accessor returns a non-empty string, not that the rendered
181    /// prose makes sense.
182    #[test]
183    fn every_rule_template_kind_matches_its_call_site() {
184        let p = prose();
185        for (rule, kind) in TEMPLATE_BINDINGS {
186            let entry = p.rule(rule);
187            let rendered = match kind {
188                TemplateKind::Static => entry.likely_cause_static().to_string(),
189                TemplateKind::Host => entry.likely_cause_with_host("example.test"),
190                TemplateKind::Peer => entry.likely_cause_with_peer("example.test"),
191                TemplateKind::OptionalField => {
192                    let with = entry.likely_cause_with_optional_field(Some("field_name"));
193                    let without = entry.likely_cause_with_optional_field(None);
194                    assert!(!with.is_empty(), "{rule}: with-field template empty");
195                    assert!(!without.is_empty(), "{rule}: no-field template empty");
196                    with
197                }
198            };
199            assert!(!rendered.is_empty(), "{rule}: rendered likely_cause empty");
200        }
201    }
202}