Repository navigation
Milestone 24: deterministic run queue overview summaries and inspection rollups #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,8 @@ pub fn dispatch( | |
| Method::RunSetDueDate => handle_run_set_due_date(params, store), | ||
| // Milestone 21: deterministic run dependency links | ||
| Method::RunSetDependencies => handle_run_set_dependencies(params, store), | ||
| // Milestone 24: deterministic queue overview | ||
| Method::RunsQueueOverview => handle_runs_queue_overview(params, store), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -518,7 +520,7 @@ fn handle_runs_list( | |
| run.is_blocking = Some(count > 0); | ||
| run.blocking_run_count = Some(count); | ||
| run.blocking_reason = if count > 0 { | ||
| Some(format!("blocking {} run(s)", count)) | ||
| Some(format!("blocking {count} run(s)")) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
@@ -573,7 +575,7 @@ fn handle_run_get( | |
| let is_blocking = Some(blocking_count > 0); | ||
| let blocking_run_count = Some(blocking_count); | ||
| let blocking_reason = if blocking_count > 0 { | ||
| Some(format!("blocking {} run(s)", blocking_count)) | ||
| Some(format!("blocking {blocking_count} run(s)")) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
@@ -1270,6 +1272,162 @@ fn handle_run_set_dependencies( | |
| Ok((serde_json::to_value(result)?, Some(state))) | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // runs.overview (Milestone 24) | ||
|
|
||
| #[allow(clippy::collapsible_if)] | ||
| /// Handle the runs.overview method - deterministic queue overview. | ||
| fn handle_runs_queue_overview( | ||
| params: serde_json::Value, | ||
| store: &Store, | ||
| ) -> Result<(serde_json::Value, Option<RunState>)> { | ||
| let p: RunsQueueOverviewParams = serde_json::from_value(params)?; | ||
|
|
||
|
Comment on lines
+1278
to
+1285
|
||
| let include_archived = p.include_archived.unwrap_or(false); | ||
| let include_snoozed = p.include_snoozed.unwrap_or(false); | ||
| let today = p.today.as_deref(); | ||
|
|
||
| // Use list_runs with high limit to get all runs for overview | ||
| // This uses the existing filter semantics from RunsListParams | ||
| let archived_only = false; // We handle filtering ourselves | ||
| let all_runs = store.list_runs( | ||
| 10000, // High limit to get all runs | ||
|
Comment on lines
+1290
to
+1294
|
||
| p.workspace_id.as_deref(), | ||
| None, // status_filter - none for overview | ||
| include_archived, | ||
| archived_only, | ||
| None, // label_filter | ||
| false, // pinned_only | ||
| include_snoozed, | ||
|
|
||
| false, // snoozed_only | ||
| )?; | ||
|
|
||
| // Use run_readiness module to derive readiness | ||
| use deterministic_core::run_readiness::derive_readiness; | ||
|
|
||
| let mut total_count = 0; | ||
| let mut ready_count = 0; | ||
| let mut blocked_count = 0; | ||
| let mut attention_count = 0; | ||
| let mut pinned_count = 0; | ||
| let mut snoozed_count = 0; | ||
| let mut overdue_count = 0; | ||
| let archived_count = 0; | ||
|
Comment on lines
+1314
to
+1315
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Archived count is always zero, making
|
||
|
|
||
|
Comment on lines
+1314
to
+1316
|
||
| let mut priority_counts = std::collections::HashMap::new(); | ||
| let mut assignee_counts = std::collections::HashMap::new(); | ||
| let mut status_counts = std::collections::HashMap::new(); | ||
|
|
||
| for run in &all_runs { | ||
| let is_archived = run.is_archived.unwrap_or(false); | ||
| let is_snoozed = run.is_snoozed.unwrap_or(false); | ||
| let is_pinned = run.is_pinned.unwrap_or(false); | ||
| let blocked_by_count = run.blocked_by_count.unwrap_or(0); | ||
|
|
||
| // Skip based on filters (list_runs handles SQL-level, we handle in-memory) | ||
| if is_snoozed && !include_snoozed { | ||
| continue; | ||
| } | ||
|
|
||
| // Count snoozed | ||
| if is_snoozed { | ||
| snoozed_count += 1; | ||
| } | ||
|
|
||
| // Count pinned | ||
| if is_pinned { | ||
| pinned_count += 1; | ||
| } | ||
|
|
||
| // Count blocked | ||
| if blocked_by_count > 0 { | ||
| blocked_count += 1; | ||
| } | ||
|
|
||
| // Derive readiness using the deterministic function | ||
| let readiness = derive_readiness( | ||
| &run.status, | ||
| is_archived, | ||
| is_snoozed, | ||
| blocked_by_count, | ||
| run.priority, | ||
| run.due_date.as_deref(), | ||
| is_pinned, | ||
| today, | ||
| ); | ||
|
|
||
| // Count ready | ||
| if readiness.is_ready { | ||
| ready_count += 1; | ||
| total_count += 1; | ||
| } else { | ||
| total_count += 1; | ||
| } | ||
|
|
||
| // Count needs attention | ||
| if readiness.needs_attention { | ||
| attention_count += 1; | ||
| } | ||
|
|
||
| // Count overdue | ||
| if today.is_some() { | ||
| if let Some(due) = &run.due_date { | ||
| if let Some(t) = today { | ||
| if due.as_str() < t { | ||
| overdue_count += 1; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+1372
to
+1381
|
||
|
|
||
| // Priority counts | ||
| let priority_str = match run.priority { | ||
| deterministic_protocol::RunPriority::Low => "low", | ||
| deterministic_protocol::RunPriority::Normal => "normal", | ||
| deterministic_protocol::RunPriority::High => "high", | ||
| deterministic_protocol::RunPriority::Urgent => "urgent", | ||
| }; | ||
| *priority_counts.entry(priority_str.to_string()).or_insert(0) += 1; | ||
|
|
||
| // Assignee counts | ||
| let assignee = run.assignee.clone().unwrap_or_else(|| "unassigned".to_string()); | ||
| *assignee_counts.entry(assignee).or_insert(0) += 1; | ||
|
|
||
| // Status counts - use prefix | ||
| let status_prefix = if run.status.starts_with("finalized:") { | ||
| "finalized" | ||
| } else if run.status.starts_with("awaiting_approval:") { | ||
| "awaiting_approval" | ||
| } else if run.status.starts_with("pending:") { | ||
| "pending" | ||
| } else { | ||
| &run.status | ||
| }; | ||
| *status_counts.entry(status_prefix.to_string()).or_insert(0) += 1; | ||
| } | ||
|
|
||
| let result = RunQueueOverview { | ||
| total_visible: total_count, | ||
| ready_count, | ||
| blocked_count, | ||
| needs_attention_count: attention_count, | ||
| pinned_count, | ||
| snoozed_count, | ||
| overdue_count: if today.is_some() { Some(overdue_count) } else { None }, | ||
| archived_count: if include_archived { Some(archived_count) } else { None }, | ||
| by_priority: PriorityCounts { | ||
|
Comment on lines
+1409
to
+1418
|
||
| low: *priority_counts.get("low").unwrap_or(&0), | ||
| normal: *priority_counts.get("normal").unwrap_or(&0), | ||
| high: *priority_counts.get("high").unwrap_or(&0), | ||
| urgent: *priority_counts.get("urgent").unwrap_or(&0), | ||
| }, | ||
| by_assignee: assignee_counts, | ||
| by_status: status_counts, | ||
| }; | ||
|
|
||
| Ok((serde_json::to_value(result)?, None)) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -913,6 +913,77 @@ pub struct RunsListResult { | |
| pub count: usize, | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // runs.overview (Milestone 24) | ||
|
|
||
| /// Parameters for requesting a queue overview summary. | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| pub struct RunsQueueOverviewParams { | ||
| /// Filter by workspace ID (optional). | ||
| #[serde(default)] | ||
| pub workspace_id: Option<String>, | ||
| /// When true, archived runs are included in counts (Milestone 13). | ||
| /// Default: false (archived runs are excluded from visible counts). | ||
| #[serde(default)] | ||
| pub include_archived: Option<bool>, | ||
| /// When true, snoozed runs are included in counts (Milestone 17). | ||
| /// Default: false (snoozed runs are excluded from visible counts). | ||
| #[serde(default)] | ||
| pub include_snoozed: Option<bool>, | ||
| /// ISO date (`YYYY-MM-DD`) for computing overdue counts (Milestone 20). | ||
| /// When provided, counts will include overdue status based on this date. | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub today: Option<String>, | ||
| } | ||
|
|
||
| /// Deterministic queue overview summary providing aggregate counts. | ||
| /// | ||
| /// This is a read-only inspection operation that derives summary counts from | ||
| /// existing run state without mutating anything. | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| pub struct RunQueueOverview { | ||
| /// Total count of visible (non-archived) runs matching the filter. | ||
| pub total_visible: usize, | ||
|
Comment on lines
+947
to
+948
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk):
|
||
| /// Count of runs that are ready to work on now. | ||
| pub ready_count: usize, | ||
| /// Count of runs that are blocked by at least one other run. | ||
| pub blocked_count: usize, | ||
| /// Count of runs that warrant operator attention. | ||
| pub needs_attention_count: usize, | ||
| /// Count of runs that are currently pinned. | ||
| pub pinned_count: usize, | ||
| /// Count of runs that are currently snoozed. | ||
| pub snoozed_count: usize, | ||
| /// Count of runs with a due date in the past (overdue). | ||
| /// Only included when `today` parameter is provided. | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub overdue_count: Option<usize>, | ||
| /// Count of archived runs (only included when include_archived is true). | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub archived_count: Option<usize>, | ||
| /// Counts grouped by priority level. | ||
| pub by_priority: PriorityCounts, | ||
| /// Counts grouped by assignee (key is assignee, value is count). | ||
| /// Unassigned runs are stored under key `"unassigned"`. | ||
| #[serde(default)] | ||
| pub by_assignee: std::collections::HashMap<String, usize>, | ||
| /// Counts grouped by status prefix (e.g., "awaiting_approval", "finalized", etc.). | ||
| #[serde(default)] | ||
| pub by_status: std::collections::HashMap<String, usize>, | ||
|
Comment on lines
+969
to
+974
|
||
| } | ||
|
|
||
| /// Priority-level counts for queue overview. | ||
| #[derive(Debug, Clone, Serialize, Deserialize, Default)] | ||
| #[serde(rename_all = "camelCase")] | ||
| pub struct PriorityCounts { | ||
| pub low: usize, | ||
| pub normal: usize, | ||
| pub high: usize, | ||
| pub urgent: usize, | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // run.get (Milestone 7) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider simplifying
handle_runs_queue_overviewby extracting small helpers, cleaning up redundant branches, and either properly tracking or removing unused counters to keep the function shallow and focused.You can simplify
handle_runs_queue_overviewwithout changing behavior by extracting a few helpers and removing redundant branching/state:total_countincrementThe
if readiness.is_ready { total_count += 1 } else { total_count += 1 }adds unnecessary branching:You can avoid nested
if today.is_some()/if letchains by matching bothtodayanddue_dateat once:The status prefix logic is inline in the loop; extracting it makes the loop easier to scan and reuse:
archived_countis currently always0, which is misleading when exposed:RunQueueOverviewto reduce mental overhead.If the loop keeps growing, consider extracting the per-run aggregation into a small function to keep
handle_runs_queue_overviewfocused on I/O and parameter handling:Then in the handler, you just initialize
Aggregatesand callaccumulate_runin the loop, keeping the top-level function shallow.