gestura_core_tasks/
verification.rs

1#![cfg(feature = "advanced-primitives")]
2
3//! Verification helpers for advanced planning primitives.
4
5use serde::{Deserialize, Serialize};
6use std::future::Future;
7
8/// Configuration for the Ralph-style verification loop.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct VerificationLoopConfig {
11    /// Whether the verification loop should run.
12    pub enabled: bool,
13    /// Maximum number of automatic retries after the initial attempt.
14    pub max_automatic_retries: u8,
15}
16
17impl Default for VerificationLoopConfig {
18    fn default() -> Self {
19        Self {
20            enabled: true,
21            max_automatic_retries: 2,
22        }
23    }
24}
25
26/// Verification requirements for prompt-like outputs.
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct PromptVerificationTargets {
29    /// Headings that must appear in the generated artifact.
30    pub required_headings: Vec<String>,
31    /// Phrases that must appear to preserve important runtime guarantees.
32    pub required_phrases: Vec<String>,
33    /// Require the headings to appear in-order.
34    pub require_ordered_headings: bool,
35    /// Require an explicit verification-oriented section.
36    pub require_verification_gate: bool,
37}
38
39/// Result of verifying a single attempt.
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct VerificationCheck {
42    /// Whether the candidate passed verification.
43    pub accepted: bool,
44    /// Missing requirements that should be repaired before retry.
45    pub missing_requirements: Vec<String>,
46}
47
48/// Record of one verification attempt.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct VerificationAttempt {
51    /// Attempt number beginning at zero.
52    pub attempt: u8,
53    /// Generated candidate that was checked.
54    pub candidate: String,
55    /// Verification result for this candidate.
56    pub check: VerificationCheck,
57}
58
59/// Aggregate verification report covering retries and final status.
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct VerificationReport {
62    /// Whether the final candidate passed verification.
63    pub passed: bool,
64    /// How many automatic retries were consumed.
65    pub automatic_retries_used: u8,
66    /// Full attempt history.
67    pub attempts: Vec<VerificationAttempt>,
68    /// Missing requirements on the final attempt, if any remain.
69    pub final_missing_requirements: Vec<String>,
70}
71
72/// Ralph-style verification loop with up to two automatic repairs.
73#[derive(Debug, Clone)]
74pub struct VerificationLoop {
75    config: VerificationLoopConfig,
76}
77
78impl VerificationLoop {
79    /// Create a new verification loop with the provided configuration.
80    pub fn new(config: VerificationLoopConfig) -> Self {
81        Self { config }
82    }
83
84    /// Run the verification loop over a generated candidate.
85    pub async fn run<Produce, ProduceFuture, Verify, VerifyFuture>(
86        &self,
87        mut produce: Produce,
88        mut verify: Verify,
89    ) -> VerificationReport
90    where
91        Produce: FnMut(u8, Option<&VerificationCheck>) -> ProduceFuture,
92        ProduceFuture: Future<Output = String>,
93        Verify: FnMut(u8, &str) -> VerifyFuture,
94        VerifyFuture: Future<Output = VerificationCheck>,
95    {
96        if !self.config.enabled {
97            let candidate = produce(0, None).await;
98            return VerificationReport {
99                passed: true,
100                automatic_retries_used: 0,
101                attempts: vec![VerificationAttempt {
102                    attempt: 0,
103                    candidate,
104                    check: VerificationCheck {
105                        accepted: true,
106                        missing_requirements: Vec::new(),
107                    },
108                }],
109                final_missing_requirements: Vec::new(),
110            };
111        }
112
113        let mut attempts = Vec::new();
114        let mut previous_check: Option<VerificationCheck> = None;
115
116        for attempt in 0..=self.config.max_automatic_retries {
117            let candidate = produce(attempt, previous_check.as_ref()).await;
118            let check = verify(attempt, &candidate).await;
119            let accepted = check.accepted;
120            attempts.push(VerificationAttempt {
121                attempt,
122                candidate,
123                check: check.clone(),
124            });
125
126            if accepted {
127                return VerificationReport {
128                    passed: true,
129                    automatic_retries_used: attempt,
130                    attempts,
131                    final_missing_requirements: Vec::new(),
132                };
133            }
134
135            previous_check = Some(check);
136        }
137
138        let final_missing_requirements = previous_check
139            .map(|check| check.missing_requirements)
140            .unwrap_or_default();
141        VerificationReport {
142            passed: false,
143            automatic_retries_used: self.config.max_automatic_retries,
144            attempts,
145            final_missing_requirements,
146        }
147    }
148}
149
150/// Verify a planning prompt against required headings and phrases.
151pub fn verify_prompt(candidate: &str, targets: &PromptVerificationTargets) -> VerificationCheck {
152    let lowered = candidate.to_ascii_lowercase();
153    let mut missing_requirements = Vec::new();
154    let mut heading_positions = Vec::new();
155
156    for heading in &targets.required_headings {
157        let needle = heading.to_ascii_lowercase();
158        match lowered.find(&needle) {
159            Some(position) => heading_positions.push(position),
160            None => missing_requirements.push(format!("missing heading `{heading}`")),
161        }
162    }
163
164    if targets.require_ordered_headings
165        && heading_positions
166            .windows(2)
167            .any(|window| window[0] > window[1])
168    {
169        missing_requirements.push("required headings are out of order".to_string());
170    }
171
172    for phrase in &targets.required_phrases {
173        if !lowered.contains(&phrase.to_ascii_lowercase()) {
174            missing_requirements.push(format!("missing phrase `{phrase}`"));
175        }
176    }
177
178    if targets.require_verification_gate && !lowered.contains("verification gate:") {
179        missing_requirements.push("missing explicit verification gate".to_string());
180    }
181
182    VerificationCheck {
183        accepted: missing_requirements.is_empty(),
184        missing_requirements,
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[tokio::test]
193    async fn verification_loop_repairs_until_prompt_passes() {
194        let loop_runner = VerificationLoop::new(VerificationLoopConfig::default());
195        let report = loop_runner
196            .run(
197                |attempt, _| async move {
198                    if attempt == 0 {
199                        "Intent anchor:\nCompletion guardrails:".to_string()
200                    } else {
201                        "Intent anchor:\nOrdered execution phases:\nVerification gate:\nCompletion guardrails:\nkeep subtasks ordered and deduplicated\ndo not mark implementation complete before verification evidence exists".to_string()
202                    }
203                },
204                |_, candidate| {
205                    let candidate = candidate.to_string();
206                    async move {
207                        verify_prompt(
208                            &candidate,
209                            &PromptVerificationTargets {
210                                required_headings: vec![
211                                    "Intent anchor:".to_string(),
212                                    "Ordered execution phases:".to_string(),
213                                    "Verification gate:".to_string(),
214                                    "Completion guardrails:".to_string(),
215                                ],
216                                required_phrases: vec![
217                                    "keep subtasks ordered and deduplicated".to_string(),
218                                    "do not mark implementation complete before verification evidence exists"
219                                        .to_string(),
220                                ],
221                                require_ordered_headings: true,
222                                require_verification_gate: true,
223                            },
224                        )
225                    }
226                },
227            )
228            .await;
229
230        assert!(report.passed);
231        assert_eq!(report.automatic_retries_used, 1);
232        assert_eq!(report.attempts.len(), 2);
233    }
234
235    #[test]
236    fn verify_prompt_reports_missing_sections() {
237        let check = verify_prompt(
238            "Intent anchor:\nCompletion guardrails:",
239            &PromptVerificationTargets {
240                required_headings: vec![
241                    "Intent anchor:".to_string(),
242                    "Ordered execution phases:".to_string(),
243                    "Verification gate:".to_string(),
244                    "Completion guardrails:".to_string(),
245                ],
246                required_phrases: vec!["keep subtasks ordered and deduplicated".to_string()],
247                require_ordered_headings: true,
248                require_verification_gate: true,
249            },
250        );
251
252        assert!(!check.accepted);
253        assert!(
254            check
255                .missing_requirements
256                .iter()
257                .any(|entry| entry.contains("Ordered execution phases"))
258        );
259    }
260}