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
+33 -21
View File
@@ -292,9 +292,12 @@ pub fn evict_item(story_id: &str) -> Result<(), String> {
///
/// Projects the loose CRDT `stage` register into a typed
/// [`crate::pipeline_state::Stage`]. Items with an unknown or missing stage
/// string are filtered out (`None`), so every `WorkItem` that escapes the
/// read path carries a valid typed stage.
/// string, or with no name set, are filtered out (`None`) — a nameless item
/// is treated as malformed and never surfaces to callers.
pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemView> {
use super::types::{Claim, EpicId};
use crate::io::story_metadata::{ItemType, QaMode};
let story_id = match item.story_id.view() {
JsonValue::String(s) if !s.is_empty() => s,
_ => return None,
@@ -303,48 +306,58 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
JsonValue::String(s) if !s.is_empty() => s,
_ => return None,
};
// AC 5: nameless item = malformed; filter it out.
let name = match item.name.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
_ => None,
JsonValue::String(s) if !s.is_empty() => s,
_ => return None,
};
let agent = match item.agent.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
_ => None,
};
let retry_count = match item.retry_count.view() {
JsonValue::Number(n) => Some(n as i64),
_ => None,
JsonValue::Number(n) if n >= 0.0 => n as u32,
_ => 0u32,
};
let depends_on = match item.depends_on.view() {
JsonValue::String(s) if !s.is_empty() => serde_json::from_str::<Vec<u32>>(&s).ok(),
_ => None,
JsonValue::String(s) if !s.is_empty() => {
serde_json::from_str::<Vec<u32>>(&s).unwrap_or_default()
}
_ => Vec::new(),
};
let claimed_by = match item.claimed_by.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
_ => None,
};
let claimed_at = match item.claimed_at.view() {
JsonValue::Number(n) if n > 0.0 => Some(n),
let claimed_at_secs = match item.claimed_at.view() {
JsonValue::Number(n) if n > 0.0 => Some(n as u64),
_ => None,
};
let merged_at = match item.merged_at.view() {
let claim = match (claimed_by, claimed_at_secs) {
(Some(node), Some(at)) => Some(Claim { node, at }),
_ => None,
};
// `merged_at` is read only to project into `Stage::Done`; it is not
// stored on `WorkItem` (callers access it via `Stage::Done { merged_at }`).
let merged_at_float = match item.merged_at.view() {
JsonValue::Number(n) if n > 0.0 => Some(n),
_ => None,
};
let qa_mode = match item.qa_mode.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
JsonValue::String(s) if !s.is_empty() => QaMode::from_str(&s),
_ => None,
};
let item_type = match item.item_type.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
JsonValue::String(s) if !s.is_empty() => ItemType::from_str(&s),
_ => None,
};
let epic = match item.epic.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
JsonValue::String(s) if !s.is_empty() => EpicId::from_crdt_str(&s),
_ => None,
};
@@ -353,7 +366,8 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
_ => None,
};
let stage = project_stage_for_view(&stage_str, &story_id, merged_at, resume_to.as_deref())?;
let stage =
project_stage_for_view(&stage_str, &story_id, merged_at_float, resume_to.as_deref())?;
Some(PipelineItemView {
story_id,
@@ -362,9 +376,7 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
agent,
retry_count,
depends_on,
claimed_by,
claimed_at,
merged_at,
claim,
qa_mode,
item_type,
epic,
@@ -571,10 +583,10 @@ mod tests {
let view = extract_item_view(&crdt.doc.items[0]).unwrap();
assert_eq!(view.story_id, "40_story_view");
assert!(matches!(view.stage, crate::pipeline_state::Stage::Qa));
assert_eq!(view.name.as_deref(), Some("View Test"));
assert_eq!(view.name, "View Test");
assert_eq!(view.agent.as_deref(), Some("coder-1"));
assert_eq!(view.retry_count, Some(2));
assert_eq!(view.depends_on, Some(vec![10, 20]));
assert_eq!(view.retry_count, 2u32);
assert_eq!(view.depends_on, vec![10u32, 20u32]);
}
#[test]