Initial scaffold: GyanDoot v2.0 in Rust
Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
152
crates/gyandoot-kanban/src/diagnostics.rs
Normal file
152
crates/gyandoot-kanban/src/diagnostics.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
//! Diagnostics — task tracking, bottlenecks, and performance metrics.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use gyandoot_core::{KanbanResult, Task, TaskStatus};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A diagnostic snapshot of the Kanban system.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KanbanDiagnostics {
|
||||
pub total_tasks: usize,
|
||||
pub backlog_count: usize,
|
||||
pub in_progress_count: usize,
|
||||
pub done_count: usize,
|
||||
pub blocked_count: usize,
|
||||
pub average_completion_time_ms: Option<f64>,
|
||||
pub bottlenecks: Vec<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Tracks task lifecycle and produces diagnostic information.
|
||||
pub struct Diagnostics {
|
||||
task_timings: HashMap<Uuid, TaskTiming>,
|
||||
bottlenecks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TaskTiming {
|
||||
created_at: DateTime<Utc>,
|
||||
started_at: Option<DateTime<Utc>>,
|
||||
completed_at: Option<DateTime<Utc>>,
|
||||
status_changes: Vec<(DateTime<Utc>, TaskStatus)>,
|
||||
}
|
||||
|
||||
impl Diagnostics {
|
||||
/// Create a new diagnostics tracker.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
task_timings: HashMap::new(),
|
||||
bottlenecks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a task creation.
|
||||
pub fn record_creation(&mut self, task_id: Uuid) {
|
||||
self.task_timings.insert(
|
||||
task_id,
|
||||
TaskTiming {
|
||||
created_at: Utc::now(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
status_changes: vec![(Utc::now(), TaskStatus::Backlog)],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Record a task status change.
|
||||
pub fn record_status_change(&mut self, task_id: &Uuid, new_status: TaskStatus) {
|
||||
if let Some(timing) = self.task_timings.get_mut(task_id) {
|
||||
timing.status_changes.push((Utc::now(), new_status.clone()));
|
||||
|
||||
if new_status == TaskStatus::InProgress && timing.started_at.is_none() {
|
||||
timing.started_at = Some(Utc::now());
|
||||
}
|
||||
|
||||
if new_status == TaskStatus::Done {
|
||||
timing.completed_at = Some(Utc::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a diagnostics snapshot for a set of tasks.
|
||||
pub fn analyze(&self, tasks: &[Task]) -> KanbanResult<KanbanDiagnostics> {
|
||||
let total = tasks.len();
|
||||
let backlog = tasks.iter().filter(|t| t.status == TaskStatus::Backlog).count();
|
||||
let in_progress = tasks.iter().filter(|t| t.status == TaskStatus::InProgress).count();
|
||||
let done = tasks.iter().filter(|t| t.status == TaskStatus::Done).count();
|
||||
let blocked = tasks.iter().filter(|t| t.status == TaskStatus::Blocked).count();
|
||||
|
||||
// Calculate average completion time
|
||||
let completion_times: Vec<f64> = self
|
||||
.task_timings
|
||||
.values()
|
||||
.filter_map(|t| {
|
||||
let start = t.started_at?;
|
||||
let end = t.completed_at?;
|
||||
Some((end - start).num_milliseconds() as f64)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let avg_completion = if completion_times.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(completion_times.iter().sum::<f64>() / completion_times.len() as f64)
|
||||
};
|
||||
|
||||
Ok(KanbanDiagnostics {
|
||||
total_tasks: total,
|
||||
backlog_count: backlog,
|
||||
in_progress_count: in_progress,
|
||||
done_count: done,
|
||||
blocked_count: blocked,
|
||||
average_completion_time_ms: avg_completion,
|
||||
bottlenecks: self.bottlenecks.clone(),
|
||||
timestamp: Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a bottleneck warning.
|
||||
pub fn report_bottleneck(&mut self, description: String) {
|
||||
self.bottlenecks.push(description);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Diagnostics {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_diagnostics_tracking() {
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
let task_id = Uuid::new_v4();
|
||||
diagnostics.record_creation(task_id);
|
||||
diagnostics.record_status_change(&task_id, TaskStatus::InProgress);
|
||||
diagnostics.record_status_change(&task_id, TaskStatus::Done);
|
||||
|
||||
let tasks = vec![Task {
|
||||
id: task_id,
|
||||
title: "Test".to_string(),
|
||||
description: String::new(),
|
||||
priority: gyandoot_core::Priority::Low,
|
||||
status: TaskStatus::Done,
|
||||
assigned_agent: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
subtasks: Vec::new(),
|
||||
dependencies: Vec::new(),
|
||||
tags: Vec::new(),
|
||||
}];
|
||||
|
||||
let report = diagnostics.analyze(&tasks).unwrap();
|
||||
assert_eq!(report.total_tasks, 1);
|
||||
assert_eq!(report.done_count, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user