huskies: merge 842

This commit is contained in:
dave
2026-04-29 15:03:07 +00:00
parent f3e4d5d072
commit db65271587
6 changed files with 808 additions and 781 deletions
+133
View File
@@ -0,0 +1,133 @@
//! Bug-item creation and listing operations.
use crate::io::story_metadata::parse_front_matter;
use std::path::Path;
use super::super::{next_item_number, slugify_name, write_story_content};
/// Create a bug file and store it in the database.
///
/// Also writes to the filesystem for backwards compatibility during migration.
/// Returns the bug_id (e.g. `"4"`).
#[allow(clippy::too_many_arguments)]
pub fn create_bug_file(
root: &Path,
name: &str,
description: &str,
steps_to_reproduce: &str,
actual_result: &str,
expected_result: &str,
acceptance_criteria: Option<&[String]>,
depends_on: Option<&[u32]>,
) -> Result<String, String> {
let bug_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 bug_id = format!("{bug_number}");
let mut content = String::new();
content.push_str("---\n");
content.push_str("type: bug\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!("# Bug {bug_number}: {name}\n\n"));
content.push_str("## Description\n\n");
content.push_str(description);
content.push_str("\n\n");
content.push_str("## How to Reproduce\n\n");
content.push_str(steps_to_reproduce);
content.push_str("\n\n");
content.push_str("## Actual Result\n\n");
content.push_str(actual_result);
content.push_str("\n\n");
content.push_str("## Expected Result\n\n");
content.push_str(expected_result);
content.push_str("\n\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("- [ ] Bug is fixed and verified\n");
}
// Write to database content store and CRDT.
write_story_content(root, &bug_id, "1_backlog", &content);
Ok(bug_id)
}
/// Returns true if the item stem is a bug item.
///
/// Checks the slug-based ID format first (e.g. `"4_bug_login_crash"`), then
/// falls back to reading `type: bug` from the content store for numeric-only IDs.
pub(super) fn is_bug_item(stem: &str) -> bool {
let after_num = stem.trim_start_matches(|c: char| c.is_ascii_digit());
if after_num.starts_with("_bug_") {
return true;
}
// Numeric-only ID: check content store front matter.
if after_num.is_empty() {
return crate::db::read_content(stem)
.and_then(|c| parse_front_matter(&c).ok())
.and_then(|m| m.item_type)
.map(|t| t == "bug")
.unwrap_or(false);
}
false
}
/// Extract bug name from content (heading or front matter).
pub(super) fn extract_bug_name_from_content(content: &str) -> Option<String> {
// Try front matter first.
if let Ok(meta) = parse_front_matter(content)
&& let Some(name) = meta.name
{
return Some(name);
}
// Fallback: heading.
for line in content.lines() {
if let Some(rest) = line.strip_prefix("# Bug ")
&& let Some(colon_pos) = rest.find(": ")
{
return Some(rest[colon_pos + 2..].to_string());
}
}
None
}
/// List all open bugs from CRDT + content store.
///
/// Returns a sorted list of `(bug_id, name)` pairs.
pub fn list_bug_files(_root: &Path) -> Result<Vec<(String, String)>, String> {
let mut bugs = Vec::new();
for item in crate::pipeline_state::read_all_typed() {
if !matches!(item.stage, crate::pipeline_state::Stage::Backlog)
|| !is_bug_item(&item.story_id.0)
{
continue;
}
let sid = item.story_id.0;
let name = if item.name.is_empty() {
None
} else {
Some(item.name)
}
.or_else(|| crate::db::read_content(&sid).and_then(|c| extract_bug_name_from_content(&c)))
.unwrap_or_else(|| sid.clone());
bugs.push((sid, name));
}
bugs.sort_by(|a, b| a.0.cmp(&b.0));
Ok(bugs)
}