huskies: merge 946

This commit is contained in:
dave
2026-05-13 07:54:50 +00:00
parent 4a0fbcaa95
commit a7840ea4b0
49 changed files with 378 additions and 314 deletions
+2 -3
View File
@@ -77,9 +77,8 @@ pub(super) fn is_bug_item(stem: &str) -> bool {
}
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 == "bug")
.unwrap_or(false);
.and_then(|v| v.item_type())
.is_some_and(|t| t == crate::io::story_metadata::ItemType::Bug);
}
false
}
+1 -1
View File
@@ -73,7 +73,7 @@ pub fn create_epic_file(
write_story_content(root, &epic_id, "1_backlog", &content, Some(name));
// Story 933: typed CRDT register for item_type.
crate::crdt_state::set_item_type(&epic_id, Some("epic"));
crate::crdt_state::set_item_type(&epic_id, Some(crate::io::story_metadata::ItemType::Epic));
Ok(epic_id)
}
+2 -3
View File
@@ -71,9 +71,8 @@ pub(super) fn is_refactor_item(stem: &str) -> bool {
}
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);
.and_then(|v| v.item_type())
.is_some_and(|t| t == crate::io::story_metadata::ItemType::Refactor);
}
false
}
+1 -1
View File
@@ -362,7 +362,7 @@ fn create_spike_file_with_special_chars_in_name_produces_valid_yaml() {
let spike_id = result.unwrap();
let view = crate::crdt_state::read_item(&spike_id).expect("CRDT entry should exist");
assert_eq!(view.name(), Some(name));
assert_eq!(view.name(), name);
}
#[test]
+28 -36
View File
@@ -103,8 +103,10 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
} else {
None
};
let qa = view.as_ref().and_then(|v| v.qa_mode().map(str::to_string));
let epic_id = view.as_ref().and_then(|v| v.epic().map(str::to_string));
let qa = view
.as_ref()
.and_then(|v| v.qa_mode().map(|q| q.as_str().to_string()));
let epic_id = view.as_ref().and_then(|v| v.epic().map(|e| e.to_string()));
let merge_failure = crate::crdt_state::read_merge_job(sid).and_then(|j| j.error);
let story = UpcomingStory {
@@ -219,7 +221,7 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
.map(|item| {
let sid = &item.story_id.0;
let epic_id =
crate::crdt_state::read_item(sid).and_then(|v| v.epic().map(str::to_string));
crate::crdt_state::read_item(sid).and_then(|v| v.epic().map(|e| e.to_string()));
UpcomingStory {
story_id: item.story_id.0.clone(),
name: if item.name.is_empty() {
@@ -265,34 +267,25 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
///
/// Story 929: validation reads the typed CRDT `name` register; the legacy YAML
/// front-matter parse is gone.
///
/// Story 946: nameless items are filtered at the CRDT layer (`extract_item_view`
/// returns `None` for items with no name register set) and therefore never reach
/// this function. Every item in `read_all_typed()` is guaranteed to have a
/// non-empty name, so the only validation left here is stage filtering.
pub fn validate_story_dirs(_root: &Path) -> Result<Vec<StoryValidationResult>, String> {
use crate::pipeline_state::Stage;
let mut results = Vec::new();
let typed_items = crate::pipeline_state::read_all_typed();
for item in typed_items {
for item in crate::pipeline_state::read_all_typed() {
if !matches!(item.stage, Stage::Backlog | Stage::Coding) {
continue;
}
let story_id = item.story_id.0.clone();
let name = crate::crdt_state::read_item(&story_id)
.and_then(|v| v.name().map(str::to_string))
.filter(|s| !s.is_empty());
if name.is_some() {
results.push(StoryValidationResult {
story_id,
valid: true,
error: None,
});
} else {
results.push(StoryValidationResult {
story_id,
valid: false,
error: Some("Missing 'name' field".to_string()),
});
}
results.push(StoryValidationResult {
story_id: item.story_id.0.clone(),
valid: true,
error: None,
});
}
results.sort_by(|a, b| a.story_id.cmp(&b.story_id));
@@ -591,12 +584,13 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let results = validate_story_dirs(tmp.path()).unwrap();
let r = results
.iter()
.find(|r| r.story_id == "9875_story_no_fm")
.unwrap();
assert!(!r.valid);
assert_eq!(r.error.as_deref(), Some("Missing 'name' field"));
// Story 946: nameless items are invisible at the CRDT layer (AC 5).
// `extract_item_view` returns `None` for items with no name register,
// so they never surface to `validate_story_dirs`.
assert!(
results.iter().all(|r| r.story_id != "9875_story_no_fm"),
"nameless items must be invisible to validate_story_dirs"
);
}
#[test]
@@ -611,13 +605,11 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let results = validate_story_dirs(tmp.path()).unwrap();
let r = results
.iter()
.find(|r| r.story_id == "9876_story_no_name")
.unwrap();
assert!(!r.valid);
let err = r.error.as_deref().unwrap();
assert!(err.contains("Missing 'name' field"));
// Story 946: nameless items are invisible at the CRDT layer (AC 5).
assert!(
results.iter().all(|r| r.story_id != "9876_story_no_name"),
"nameless items must be invisible to validate_story_dirs"
);
}
#[test]
+2 -2
View File
@@ -185,7 +185,7 @@ mod tests {
let story_id = result.unwrap();
let view =
crate::crdt_state::read_item(&story_id).expect("CRDT entry should exist after create");
assert_eq!(view.name(), Some(name));
assert_eq!(view.name(), name);
}
// ── check_criterion_in_file tests ─────────────────────────────────────────
@@ -242,7 +242,7 @@ mod tests {
let view = crate::crdt_state::read_item(&story_id).expect("CRDT entry must exist");
assert_eq!(
view.item_type(),
Some("story"),
Some(crate::io::story_metadata::ItemType::Story),
"CRDT register must be set to story"
);
}
@@ -435,7 +435,10 @@ mod tests {
setup_story_in_fs(tmp.path(), "100_spike_my_spike", spike_content);
// Convert spike to story by updating the typed item_type CRDT register.
crate::crdt_state::set_item_type("100_spike_my_spike", Some("story"));
crate::crdt_state::set_item_type(
"100_spike_my_spike",
Some(crate::io::story_metadata::ItemType::Story),
);
// Add three acceptance criteria.
add_criterion_to_file(tmp.path(), "100_spike_my_spike", "First criterion")
+4 -1
View File
@@ -266,7 +266,10 @@ pub(crate) fn create_item_in_backlog(
write_story_content(root, &item_id, "1_backlog", &content, Some(name));
crate::crdt_state::set_depends_on(&item_id, depends_on.unwrap_or(&[]));
crate::crdt_state::set_item_type(&item_id, Some(item_type));
crate::crdt_state::set_item_type(
&item_id,
crate::io::story_metadata::ItemType::from_str(item_type),
);
Ok(item_id)
}