1use crate::workflow::contract::{ContractParser, ParsedOutput, StepStatus};
10use anyhow::{Context, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Story {
17 pub id: String,
19 pub title: String,
21 pub description: String,
23 #[serde(default)]
25 pub acceptance_criteria: Vec<String>,
26 #[serde(default)]
28 pub test_criteria: Vec<String>,
29 #[serde(default)]
31 pub depends_on: Vec<String>,
32 #[serde(default)]
34 pub effort: Option<String>,
35 #[serde(default)]
37 pub completed: bool,
38 #[serde(default)]
40 pub retry_count: u32,
41 #[serde(default)]
43 pub verified: Option<bool>,
44 #[serde(default)]
46 pub verify_feedback: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct StoryLoopConfig {
52 pub over: String,
54 #[serde(default)]
56 pub completion: CompletionCondition,
57 #[serde(default)]
59 pub fresh_session: bool,
60 #[serde(default)]
62 pub verify_each: bool,
63 #[serde(default)]
65 pub verify_step: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70#[serde(rename_all = "snake_case")]
71pub enum CompletionCondition {
72 #[default]
74 AllDone,
75 AtLeastOne,
77 Count(usize),
79 Percentage(f32),
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct StoryLoopState {
86 pub stories: Vec<Story>,
88 pub current_index: usize,
90 pub completed_ids: Vec<String>,
92 pub pending_verification: Vec<String>,
94 pub iteration_count: usize,
96 pub max_iterations: usize,
98}
99
100impl StoryLoopState {
101 pub fn new(stories: Vec<Story>) -> Self {
103 Self {
104 max_iterations: stories.len() * 3, stories,
106 current_index: 0,
107 completed_ids: vec![],
108 pending_verification: vec![],
109 iteration_count: 0,
110 }
111 }
112
113 pub fn current_story(&self) -> Option<&Story> {
115 self.stories.get(self.current_index)
116 }
117
118 pub fn current_story_mut(&mut self) -> Option<&mut Story> {
120 self.stories.get_mut(self.current_index)
121 }
122
123 pub fn mark_completed(&mut self) -> Result<()> {
125 if let Some(story) = self.current_story() {
126 let id = story.id.clone();
127 if let Some(story) = self.stories.iter_mut().find(|s| s.id == id) {
128 story.completed = true;
129 if !self.completed_ids.contains(&id) {
130 self.completed_ids.push(id);
131 }
132 }
133 }
134 Ok(())
135 }
136
137 pub fn mark_retry(&mut self, feedback: &str) -> Result<()> {
139 if let Some(story) = self.current_story_mut() {
140 story.retry_count += 1;
141 story.verify_feedback = Some(feedback.to_string());
142 }
143 Ok(())
144 }
145
146 pub fn next_story(&mut self) -> bool {
148 self.iteration_count += 1;
149
150 for i in (self.current_index + 1)..self.stories.len() {
152 if !self.stories[i].completed {
153 self.current_index = i;
154 return true;
155 }
156 }
157
158 for i in 0..self.stories.len() {
160 if !self.stories[i].completed && self.stories[i].retry_count < 2 {
161 self.current_index = i;
162 return true;
163 }
164 }
165
166 false
167 }
168
169 pub fn is_complete(&self, condition: &CompletionCondition) -> bool {
171 let completed = self.completed_ids.len();
172 let total = self.stories.len();
173
174 match condition {
175 CompletionCondition::AllDone => completed >= total,
176 CompletionCondition::AtLeastOne => completed >= 1,
177 CompletionCondition::Count(n) => completed >= *n,
178 CompletionCondition::Percentage(p) => {
179 if total == 0 {
180 true
181 } else {
182 (completed as f32 / total as f32) >= (*p / 100.0)
183 }
184 }
185 }
186 }
187
188 pub fn should_stop(&self) -> bool {
190 self.iteration_count >= self.max_iterations
191 }
192
193 pub fn completion_percentage(&self) -> f32 {
195 if self.stories.is_empty() {
196 100.0
197 } else {
198 (self.completed_ids.len() as f32 / self.stories.len() as f32) * 100.0
199 }
200 }
201
202 pub fn stories_json(&self) -> Result<String> {
204 serde_json::to_string(&self.stories).context("Failed to serialize stories")
205 }
206
207 pub fn context_variables(&self) -> HashMap<String, String> {
209 let mut vars = HashMap::new();
210
211 vars.insert("stories_count".to_string(), self.stories.len().to_string());
212 vars.insert(
213 "completed_count".to_string(),
214 self.completed_ids.len().to_string(),
215 );
216 vars.insert(
217 "remaining_count".to_string(),
218 (self.stories.len() - self.completed_ids.len()).to_string(),
219 );
220 vars.insert(
221 "completion_percentage".to_string(),
222 format!("{:.1}", self.completion_percentage()),
223 );
224
225 if let Some(story) = self.current_story() {
226 vars.insert("current_story_id".to_string(), story.id.clone());
227 vars.insert("current_story_title".to_string(), story.title.clone());
228 vars.insert(
229 "current_story".to_string(),
230 serde_json::to_string(story).unwrap_or_default(),
231 );
232 vars.insert(
233 "current_story_description".to_string(),
234 story.description.clone(),
235 );
236
237 if let Some(feedback) = &story.verify_feedback {
238 vars.insert("verify_feedback".to_string(), feedback.clone());
239 }
240 }
241
242 vars.insert(
243 "completed_stories".to_string(),
244 serde_json::to_string(&self.completed_ids).unwrap_or_default(),
245 );
246
247 vars.insert(
248 "stories_json".to_string(),
249 self.stories_json().unwrap_or_default(),
250 );
251
252 vars
253 }
254}
255
256pub struct StoryLoopExecutor;
258
259impl StoryLoopExecutor {
260 pub async fn execute_loop<F, Fut>(
262 config: &StoryLoopConfig,
263 stories: Vec<Story>,
264 mut step_fn: F,
265 ) -> Result<StoryLoopResult>
266 where
267 F: FnMut(StoryLoopState) -> Fut,
268 Fut: std::future::Future<Output = Result<ParsedOutput>>,
269 {
270 let mut state = StoryLoopState::new(stories);
271
272 loop {
273 if state.should_stop() {
275 return Ok(StoryLoopResult {
276 success: false,
277 state,
278 reason: Some("Max iterations exceeded".to_string()),
279 });
280 }
281
282 if state.is_complete(&config.completion) {
283 return Ok(StoryLoopResult {
284 success: true,
285 state,
286 reason: None,
287 });
288 }
289
290 if state.current_story().is_none() {
292 return Ok(StoryLoopResult {
293 success: false,
294 state,
295 reason: Some("No more stories to process".to_string()),
296 });
297 }
298
299 let output = step_fn(state.clone()).await?;
301
302 match output.status {
304 StepStatus::Done => {
305 state.mark_completed()?;
306
307 if config.verify_each {
309 if let Some(story) = state.current_story() {
310 state.pending_verification.push(story.id.clone());
311 }
312 }
313
314 state.next_story();
315 }
316 StepStatus::Retry => {
317 let feedback = ContractParser::get_feedback(&output.raw_output, "ISSUES")
318 .unwrap_or_else(|| "Retry requested".to_string());
319
320 let current_story_id = state.current_story().map(|s| s.id.clone());
322
323 state.mark_retry(&feedback)?;
324
325 if let Some(story_id) = current_story_id {
327 if let Some(story) = state.stories.iter().find(|s| s.id == story_id) {
328 if story.retry_count >= 2 {
329 return Ok(StoryLoopResult {
330 success: false,
331 state,
332 reason: Some(format!(
333 "Max retries reached for story: {}",
334 story_id
335 )),
336 });
337 }
338 }
339 }
340 }
341 StepStatus::Blocked => {
342 let blocked_story_id = state
343 .current_story()
344 .map(|s| s.id.clone())
345 .unwrap_or_default();
346 return Ok(StoryLoopResult {
347 success: false,
348 state,
349 reason: Some(format!("Story blocked: {}", blocked_story_id)),
350 });
351 }
352 }
353 }
354 }
355}
356
357#[derive(Debug, Clone)]
359pub struct StoryLoopResult {
360 pub success: bool,
362 pub state: StoryLoopState,
364 pub reason: Option<String>,
366}
367
368pub fn parse_stories(json_str: &str) -> Result<Vec<Story>> {
370 serde_json::from_str(json_str).context("Failed to parse stories JSON")
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn create_test_stories() -> Vec<Story> {
378 vec![
379 Story {
380 id: "story-1".to_string(),
381 title: "First Story".to_string(),
382 description: "Implement feature A".to_string(),
383 acceptance_criteria: vec!["Feature A works".to_string()],
384 test_criteria: vec!["Test A passes".to_string()],
385 depends_on: vec![],
386 effort: Some("small".to_string()),
387 completed: false,
388 retry_count: 0,
389 verified: None,
390 verify_feedback: None,
391 },
392 Story {
393 id: "story-2".to_string(),
394 title: "Second Story".to_string(),
395 description: "Implement feature B".to_string(),
396 acceptance_criteria: vec!["Feature B works".to_string()],
397 test_criteria: vec!["Test B passes".to_string()],
398 depends_on: vec!["story-1".to_string()],
399 effort: Some("medium".to_string()),
400 completed: false,
401 retry_count: 0,
402 verified: None,
403 verify_feedback: None,
404 },
405 ]
406 }
407
408 #[test]
409 fn test_story_loop_state() {
410 let stories = create_test_stories();
411 let state = StoryLoopState::new(stories);
412
413 assert_eq!(state.stories.len(), 2);
414 assert_eq!(state.current_index, 0);
415 assert!(state.current_story().is_some());
416 assert_eq!(state.current_story().unwrap().id, "story-1");
417 }
418
419 #[test]
420 fn test_mark_completed() {
421 let stories = create_test_stories();
422 let mut state = StoryLoopState::new(stories);
423
424 state.mark_completed().unwrap();
425 assert_eq!(state.completed_ids.len(), 1);
426 assert!(state.stories[0].completed);
427 }
428
429 #[test]
430 fn test_next_story() {
431 let stories = create_test_stories();
432 let mut state = StoryLoopState::new(stories);
433
434 assert_eq!(state.current_story().unwrap().id, "story-1");
436
437 state.mark_completed().unwrap();
439 assert!(state.next_story());
440 assert_eq!(state.current_story().unwrap().id, "story-2");
441 }
442
443 #[test]
444 fn test_completion_conditions() {
445 let stories = create_test_stories();
446 let mut state = StoryLoopState::new(stories);
447
448 assert!(!state.is_complete(&CompletionCondition::AllDone));
450 assert!(!state.is_complete(&CompletionCondition::Count(1)));
451
452 state.mark_completed().unwrap();
454 assert!(state.is_complete(&CompletionCondition::Count(1)));
455 assert!(!state.is_complete(&CompletionCondition::AllDone));
456
457 state.next_story();
459 state.mark_completed().unwrap();
460 assert!(state.is_complete(&CompletionCondition::AllDone));
461 }
462
463 #[test]
464 fn test_context_variables() {
465 let stories = create_test_stories();
466 let state = StoryLoopState::new(stories);
467
468 let vars = state.context_variables();
469 assert_eq!(vars.get("stories_count").unwrap(), "2");
470 assert_eq!(vars.get("completed_count").unwrap(), "0");
471 assert!(vars.contains_key("current_story"));
472 assert!(vars.contains_key("stories_json"));
473 }
474
475 #[test]
476 fn test_parse_stories() {
477 let json = r#"[
478 {
479 "id": "story-1",
480 "title": "Test Story",
481 "description": "A test story",
482 "acceptance_criteria": ["It works"],
483 "completed": false
484 }
485 ]"#;
486
487 let stories = parse_stories(json).unwrap();
488 assert_eq!(stories.len(), 1);
489 assert_eq!(stories[0].id, "story-1");
490 }
491}