2026-04-29 15:03:07 +00:00
|
|
|
//! 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<String, String> {
|
|
|
|
|
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<String> = 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.
|
2026-05-12 20:55:25 +01:00
|
|
|
write_story_content(root, &refactor_id, "1_backlog", &content, Some(name));
|
2026-04-29 15:03:07 +00:00
|
|
|
|
2026-04-29 15:54:33 +00:00
|
|
|
// Sync depends_on to the typed CRDT register.
|
|
|
|
|
crate::crdt_state::set_depends_on(&refactor_id, depends_on.unwrap_or(&[]));
|
|
|
|
|
|
2026-05-12 19:58:43 +01:00
|
|
|
// Story 933: typed CRDT register for item_type.
|
|
|
|
|
crate::crdt_state::set_item_type(&refactor_id, Some("refactor"));
|
|
|
|
|
|
2026-04-29 15:03:07 +00:00
|
|
|
Ok(refactor_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns true if the item stem is a refactor item.
|
|
|
|
|
///
|
2026-05-12 19:58:43 +01:00
|
|
|
/// 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).
|
2026-04-29 15:03:07 +00:00
|
|
|
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() {
|
2026-05-12 19:58:43 +01:00
|
|
|
return crate::crdt_state::read_item(stem)
|
|
|
|
|
.and_then(|v| v.item_type().map(str::to_string))
|
2026-04-29 15:03:07 +00:00
|
|
|
.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<Vec<(String, String)>, 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() {
|
2026-05-12 20:13:17 +01:00
|
|
|
sid.clone()
|
2026-04-29 15:03:07 +00:00
|
|
|
} else {
|
2026-05-12 20:13:17 +01:00
|
|
|
item.name
|
|
|
|
|
};
|
2026-04-29 15:03:07 +00:00
|
|
|
refactors.push((sid, name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refactors.sort_by(|a, b| a.0.cmp(&b.0));
|
|
|
|
|
Ok(refactors)
|
|
|
|
|
}
|