huskies: merge 951

This commit is contained in:
dave
2026-05-13 04:28:30 +00:00
parent c5abc44a63
commit 2f50e2198b
12 changed files with 178 additions and 218 deletions
+94 -3
View File
@@ -295,12 +295,17 @@ pub fn evict_item(story_id: &str) -> Result<(), String> {
}
/// Extract a `PipelineItemView` from a `PipelineItemCrdt`.
///
/// 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.
pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemView> {
let story_id = match item.story_id.view() {
JsonValue::String(s) if !s.is_empty() => s,
_ => return None,
};
let stage = match item.stage.view() {
let stage_str = match item.stage.view() {
JsonValue::String(s) if !s.is_empty() => s,
_ => return None,
};
@@ -368,6 +373,8 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
_ => None,
};
let stage = project_stage_for_view(&stage_str, &story_id, merged_at, blocked)?;
Some(PipelineItemView {
story_id,
stage,
@@ -388,6 +395,90 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
})
}
/// Project the loose `stage` string from the CRDT into a typed
/// [`crate::pipeline_state::Stage`].
///
/// Rich variants synthesise payload fields from sibling registers (or sane
/// defaults). Returns `None` for unknown stage strings — the read path drops
/// the entry so no caller ever sees a stage it can't pattern-match against.
///
/// Accepts BOTH the clean post-934 wire vocabulary (`"backlog"`, `"coding"`,
/// `"qa"`, etc.) and pre-934 directory-style strings (`"1_backlog"`,
/// `"2_current"`, etc.) — legacy strings are normalised to their clean form
/// before the typed projection. This keeps remote ops from older nodes (and
/// raw-CRDT test inserts that bypass `migrate_legacy_stage_strings`) from
/// silently disappearing from the typed read path.
fn project_stage_for_view(
stage_str: &str,
story_id: &str,
merged_at: Option<f64>,
blocked: Option<bool>,
) -> Option<crate::pipeline_state::Stage> {
use crate::pipeline_state::{ArchiveReason, BranchName, GitSha, Stage};
use chrono::{DateTime, Utc};
use std::num::NonZeroU32;
// Normalise legacy directory-style strings to their clean wire form so
// the match below stays single-shape.
let clean = match stage_str {
"0_upcoming" => "upcoming",
"1_backlog" => "backlog",
"2_current" => "coding",
"2_blocked" => "blocked",
"3_qa" => "qa",
"4_merge" => "merge",
"4_merge_failure" => "merge_failure",
"5_done" => "done",
"6_archived" => "archived",
// Pre-934 `7_frozen` collapses to backlog (the frozen flag is an
// orthogonal CRDT register since story 934 stage 4).
"7_frozen" => "backlog",
other => other,
};
match clean {
"upcoming" => Some(Stage::Upcoming),
"backlog" => Some(Stage::Backlog),
"coding" => Some(Stage::Coding),
"qa" => Some(Stage::Qa),
"blocked" => Some(Stage::Blocked {
reason: String::new(),
}),
"merge" => Some(Stage::Merge {
feature_branch: BranchName(format!("feature/story-{story_id}")),
commits_ahead: NonZeroU32::new(1).expect("1 is non-zero"),
}),
"merge_failure" => Some(Stage::MergeFailure {
reason: String::new(),
}),
"done" => {
let merged_at = merged_at
.map(|ts| {
DateTime::from_timestamp(ts as i64, 0).unwrap_or(DateTime::<Utc>::UNIX_EPOCH)
})
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH);
Some(Stage::Done {
merged_at,
merge_commit: GitSha("legacy".to_string()),
})
}
"archived" => {
let reason = if blocked.unwrap_or(false) {
ArchiveReason::Blocked {
reason: "migrated from legacy blocked field".to_string(),
}
} else {
ArchiveReason::Completed
};
Some(Stage::Archived {
archived_at: Utc::now(),
reason,
})
}
_ => None,
}
}
/// Check whether a dependency (by numeric ID prefix) is in `5_done` or `6_archived`
/// according to CRDT state.
///
@@ -478,7 +569,7 @@ mod tests {
let item_json: JsonValue = json!({
"story_id": "40_story_view",
"stage": "3_qa",
"stage": "qa",
"name": "View Test",
"agent": "coder-1",
"retry_count": 2.0,
@@ -494,7 +585,7 @@ mod tests {
let view = extract_item_view(&crdt.doc.items[0]).unwrap();
assert_eq!(view.story_id, "40_story_view");
assert_eq!(view.stage, "3_qa");
assert!(matches!(view.stage, crate::pipeline_state::Stage::Qa));
assert_eq!(view.name.as_deref(), Some("View Test"));
assert_eq!(view.agent.as_deref(), Some("coder-1"));
assert_eq!(view.retry_count, Some(2));