huskies: merge 1026
This commit is contained in:
@@ -6,11 +6,16 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use super::super::{next_item_number, slugify_name, write_story_content};
|
||||
use super::super::create_item_in_backlog;
|
||||
|
||||
/// Create an epic file and store it in the database.
|
||||
/// Create an epic file, storing it in the database via `create_item_in_backlog`.
|
||||
///
|
||||
/// Returns the epic_id (e.g. `"880"`).
|
||||
/// Routes through `create_item_in_backlog` so that the tombstone-skip allocator
|
||||
/// defence and post-write CRDT verification (added by bug 1001's fix) apply to
|
||||
/// epics too. Previously this function had its own ad-hoc allocate-and-write
|
||||
/// path that bypassed those safety checks.
|
||||
///
|
||||
/// Returns the epic_id (numeric only, e.g. `"880"`).
|
||||
pub fn create_epic_file(
|
||||
root: &Path,
|
||||
name: &str,
|
||||
@@ -19,61 +24,54 @@ pub fn create_epic_file(
|
||||
key_files: Option<&str>,
|
||||
success_criteria: Option<&[String]>,
|
||||
) -> Result<String, String> {
|
||||
let epic_number = next_item_number(root)?;
|
||||
let slug = slugify_name(name);
|
||||
let name_owned = name.to_string();
|
||||
let goal_owned = goal.to_string();
|
||||
let motivation_owned = motivation.map(str::to_string);
|
||||
let key_files_owned = key_files.map(str::to_string);
|
||||
let success_criteria_owned: Vec<String> =
|
||||
success_criteria.map(|sc| sc.to_vec()).unwrap_or_default();
|
||||
|
||||
if slug.is_empty() {
|
||||
return Err("Name must contain at least one alphanumeric character.".to_string());
|
||||
}
|
||||
// Epics don't have acceptance criteria; pass an empty slice.
|
||||
// create_item_in_backlog skips the AC check for type "epic".
|
||||
create_item_in_backlog(root, "epic", name, &[], None, move |epic_number| {
|
||||
let mut content = String::new();
|
||||
content.push_str("---\n");
|
||||
content.push_str("type: epic\n");
|
||||
content.push_str(&format!("name: \"{}\"\n", name_owned.replace('"', "\\\"")));
|
||||
content.push_str("---\n\n");
|
||||
content.push_str(&format!("# Epic {epic_number}: {name_owned}\n\n"));
|
||||
|
||||
let epic_id = format!("{epic_number}");
|
||||
content.push_str("## Goal\n\n");
|
||||
content.push_str(&goal_owned);
|
||||
content.push_str("\n\n");
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str("---\n");
|
||||
content.push_str("type: epic\n");
|
||||
content.push_str(&format!("name: \"{}\"\n", name.replace('"', "\\\"")));
|
||||
content.push_str("---\n\n");
|
||||
content.push_str(&format!("# Epic {epic_number}: {name}\n\n"));
|
||||
|
||||
content.push_str("## Goal\n\n");
|
||||
content.push_str(goal);
|
||||
content.push_str("\n\n");
|
||||
|
||||
content.push_str("## Motivation\n\n");
|
||||
if let Some(m) = motivation {
|
||||
content.push_str(m);
|
||||
content.push('\n');
|
||||
} else {
|
||||
content.push_str("- TBD\n");
|
||||
}
|
||||
content.push('\n');
|
||||
|
||||
content.push_str("## Key Files\n\n");
|
||||
if let Some(kf) = key_files {
|
||||
content.push_str(kf);
|
||||
content.push('\n');
|
||||
} else {
|
||||
content.push_str("- TBD\n");
|
||||
}
|
||||
content.push('\n');
|
||||
|
||||
content.push_str("## Success Criteria\n\n");
|
||||
match success_criteria {
|
||||
Some(criteria) if !criteria.is_empty() => {
|
||||
for c in criteria {
|
||||
content.push_str(&format!("- {c}\n"));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
content.push_str("## Motivation\n\n");
|
||||
if let Some(ref m) = motivation_owned {
|
||||
content.push_str(m);
|
||||
content.push('\n');
|
||||
} else {
|
||||
content.push_str("- TBD\n");
|
||||
}
|
||||
}
|
||||
content.push('\n');
|
||||
|
||||
// Epics are stored in backlog (no pipeline advancement).
|
||||
write_story_content(root, &epic_id, "1_backlog", &content, Some(name));
|
||||
content.push_str("## Key Files\n\n");
|
||||
if let Some(ref kf) = key_files_owned {
|
||||
content.push_str(kf);
|
||||
content.push('\n');
|
||||
} else {
|
||||
content.push_str("- TBD\n");
|
||||
}
|
||||
content.push('\n');
|
||||
|
||||
// Story 933: typed CRDT register for item_type.
|
||||
crate::crdt_state::set_item_type(&epic_id, Some(crate::io::story_metadata::ItemType::Epic));
|
||||
content.push_str("## Success Criteria\n\n");
|
||||
if !success_criteria_owned.is_empty() {
|
||||
for c in &success_criteria_owned {
|
||||
content.push_str(&format!("- {c}\n"));
|
||||
}
|
||||
} else {
|
||||
content.push_str("- TBD\n");
|
||||
}
|
||||
|
||||
Ok(epic_id)
|
||||
content
|
||||
})
|
||||
}
|
||||
|
||||
@@ -250,15 +250,17 @@ pub(crate) fn create_item_in_backlog(
|
||||
if slugify_name(name).is_empty() {
|
||||
return Err("Title must contain at least one alphanumeric character.".to_string());
|
||||
}
|
||||
if acceptance_criteria.is_empty() {
|
||||
return Err("At least one acceptance criterion is required.".to_string());
|
||||
}
|
||||
const VALID_TYPES: &[&str] = &["story", "bug", "spike", "refactor"];
|
||||
const VALID_TYPES: &[&str] = &["story", "bug", "spike", "refactor", "epic"];
|
||||
if !VALID_TYPES.contains(&item_type) {
|
||||
return Err(format!(
|
||||
"Invalid item type '{item_type}': must be one of story, bug, spike, refactor."
|
||||
"Invalid item type '{item_type}': must be one of story, bug, spike, refactor, epic."
|
||||
));
|
||||
}
|
||||
// Epics use success_criteria (optional); the acceptance_criteria check is
|
||||
// only meaningful for pipeline work items.
|
||||
if item_type != "epic" && acceptance_criteria.is_empty() {
|
||||
return Err("At least one acceptance criterion is required.".to_string());
|
||||
}
|
||||
|
||||
let item_number = next_item_number(root)?;
|
||||
let item_id = format!("{item_number}");
|
||||
|
||||
Reference in New Issue
Block a user