huskies: merge 1141 story Convert work-item type between spike/story/bug/refactor (or at least spike→story)

This commit is contained in:
dave
2026-05-18 14:50:00 +00:00
parent 5062e008c6
commit 95c0aafb68
12 changed files with 487 additions and 7 deletions
+4 -3
View File
@@ -22,7 +22,8 @@ pub use newtypes::{
AcceptanceCriterion, DependsOnId, Description, StoryId, StoryName, TargetStage,
};
pub use requests::{
AddCriterionRequest, CreateBugRequest, CreateEpicRequest, CreateRefactorRequest,
CreateSpikeRequest, CreateStoryRequest, EditCriterionRequest, FreezeStoryRequest,
MoveStoryRequest, MoveStoryToMergeRequest, UnblockStoryRequest, UpdateStoryRequest,
AddCriterionRequest, ConvertItemTypeRequest, CreateBugRequest, CreateEpicRequest,
CreateRefactorRequest, CreateSpikeRequest, CreateStoryRequest, EditCriterionRequest,
FreezeStoryRequest, MoveStoryRequest, MoveStoryToMergeRequest, UnblockStoryRequest,
UpdateStoryRequest,
};
+75
View File
@@ -1212,6 +1212,81 @@ impl FreezeStoryRequest {
})
}
}
// ---------------------------------------------------------------------------
// ConvertItemTypeRequest
// ---------------------------------------------------------------------------
/// Fully validated inputs for the `convert_item_type` MCP tool and `convert` chat command.
#[derive(Debug)]
pub struct ConvertItemTypeRequest {
/// Validated story identifier.
pub story_id: StoryId,
/// The target item type.
pub new_type: crate::io::story_metadata::ItemType,
}
impl ConvertItemTypeRequest {
/// Parse and validate a `convert_item_type` JSON argument map.
///
/// Required fields: `story_id` (work-item filename stem), `new_type`
/// (one of `"story"`, `"bug"`, `"spike"`, `"refactor"`, `"epic"`).
pub fn from_json(args: &serde_json::Value) -> Result<Self, String> {
let mut errors: Vec<ValidationError> = Vec::new();
let story_id = match args.get("story_id").and_then(|v| v.as_str()) {
None => {
errors.push(ValidationError::FieldMissing {
field: "story_id".into(),
});
None
}
Some(raw) => match StoryId::parse(raw) {
Ok(id) => Some(id),
Err(mut errs) => {
errors.append(&mut errs);
None
}
},
};
let new_type = match args.get("new_type").and_then(|v| v.as_str()) {
None => {
errors.push(ValidationError::FieldMissing {
field: "new_type".into(),
});
None
}
Some(raw) => match crate::io::story_metadata::ItemType::from_str(raw) {
Some(t) => Some(t),
None => {
errors.push(ValidationError::InvalidValue {
field: "new_type".into(),
actual: raw.to_string(),
allowed: vec![
"story".into(),
"bug".into(),
"spike".into(),
"refactor".into(),
"epic".into(),
],
});
None
}
},
};
if !errors.is_empty() {
return Err(format_errors_as_json(&errors));
}
Ok(ConvertItemTypeRequest {
story_id: story_id.unwrap(),
new_type: new_type.unwrap(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;