//! Refactor-item creation and listing operations. use std::path::Path; use super::super::{next_item_number, slugify_name, write_story_content}; /// Create a refactor work item and store it in the database. /// /// Returns the refactor_id (e.g. `"5"`). pub fn create_refactor_file( root: &Path, name: &str, description: Option<&str>, acceptance_criteria: Option<&[String]>, depends_on: Option<&[u32]>, ) -> Result { let refactor_number = next_item_number(root)?; let slug = slugify_name(name); if slug.is_empty() { return Err("Name must contain at least one alphanumeric character.".to_string()); } let refactor_id = format!("{refactor_number}"); let mut content = String::new(); content.push_str("---\n"); content.push_str("type: refactor\n"); content.push_str(&format!("name: \"{}\"\n", name.replace('"', "\\\""))); if let Some(deps) = depends_on.filter(|d| !d.is_empty()) { let nums: Vec = deps.iter().map(|n| n.to_string()).collect(); content.push_str(&format!("depends_on: [{}]\n", nums.join(", "))); } content.push_str("---\n\n"); content.push_str(&format!("# Refactor {refactor_number}: {name}\n\n")); content.push_str("## Current State\n\n"); content.push_str("- TBD\n\n"); content.push_str("## Desired State\n\n"); if let Some(desc) = description { content.push_str(desc); content.push('\n'); } else { content.push_str("- TBD\n"); } content.push('\n'); content.push_str("## Acceptance Criteria\n\n"); if let Some(criteria) = acceptance_criteria { for criterion in criteria { content.push_str(&format!("- [ ] {criterion}\n")); } } else { content.push_str("- [ ] Refactoring complete and all tests pass\n"); } content.push('\n'); content.push_str("## Out of Scope\n\n"); content.push_str("- TBD\n"); // Write to database content store and CRDT. write_story_content(root, &refactor_id, "1_backlog", &content, Some(name)); // Sync depends_on to the typed CRDT register. crate::crdt_state::set_depends_on(&refactor_id, depends_on.unwrap_or(&[])); // Story 933: typed CRDT register for item_type. crate::crdt_state::set_item_type(&refactor_id, Some("refactor")); Ok(refactor_id) } /// Returns true if the item stem is a refactor item. /// /// Checks the slug-based ID format first (e.g. `"5_refactor_split_agents_rs"`), /// then consults the typed CRDT `item_type` register for numeric-only IDs (933). pub(super) fn is_refactor_item(stem: &str) -> bool { let after_num = stem.trim_start_matches(|c: char| c.is_ascii_digit()); if after_num.starts_with("_refactor_") { return true; } if after_num.is_empty() { return crate::crdt_state::read_item(stem) .and_then(|v| v.item_type().map(str::to_string)) .map(|t| t == "refactor") .unwrap_or(false); } false } /// List all open refactors from CRDT + content store. /// /// Returns a sorted list of `(refactor_id, name)` pairs. pub fn list_refactor_files(_root: &Path) -> Result, String> { let mut refactors = Vec::new(); for item in crate::pipeline_state::read_all_typed() { if !matches!(item.stage, crate::pipeline_state::Stage::Backlog) || !is_refactor_item(&item.story_id.0) { continue; } let sid = item.story_id.0; let name = if item.name.is_empty() { sid.clone() } else { item.name }; refactors.push((sid, name)); } refactors.sort_by(|a, b| a.0.cmp(&b.0)); Ok(refactors) }