use super::expertise::RenderContext;
use crate::context::TaskHealth;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DetectedContext {
pub task_type: Option<String>,
pub task_health: Option<TaskHealth>,
pub user_states: Vec<String>,
pub confidence: Option<ConfidenceScores>,
pub detected_by: Vec<String>,
pub render: RenderContext,
}
impl DetectedContext {
pub fn new() -> Self {
Self {
task_type: None,
task_health: None,
user_states: Vec::new(),
confidence: None,
detected_by: Vec::new(),
render: RenderContext::default(),
}
}
pub fn with_task_type(mut self, task_type: impl Into<String>) -> Self {
self.task_type = Some(task_type.into());
self.update_render();
self
}
pub fn with_task_health(mut self, health: TaskHealth) -> Self {
self.task_health = Some(health);
self.update_render();
self
}
pub fn with_user_state(mut self, state: impl Into<String>) -> Self {
self.user_states.push(state.into());
self.update_render();
self
}
pub fn with_confidence(mut self, confidence: ConfidenceScores) -> Self {
self.confidence = Some(confidence);
self
}
pub fn detected_by(mut self, detector_name: impl Into<String>) -> Self {
self.detected_by.push(detector_name.into());
self
}
fn update_render(&mut self) {
let mut render = RenderContext::default();
if let Some(task_type) = &self.task_type {
render = render.with_task_type(task_type.clone());
}
if let Some(health) = self.task_health {
render = render.with_task_health(health);
}
for state in &self.user_states {
render = render.with_user_state(state.clone());
}
self.render = render;
}
pub fn to_render_context(&self) -> RenderContext {
self.render.clone()
}
pub fn merge(mut self, other: DetectedContext) -> Self {
if self.task_type.is_none() {
self.task_type = other.task_type;
}
if other.task_health.is_some() {
self.task_health = other.task_health;
}
for state in other.user_states {
if !self.user_states.contains(&state) {
self.user_states.push(state);
}
}
if let Some(other_conf) = other.confidence {
self.confidence = Some(if let Some(existing) = self.confidence {
existing.merge(other_conf)
} else {
other_conf
});
}
self.detected_by.extend(other.detected_by);
self.update_render();
self
}
}
impl Default for DetectedContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConfidenceScores {
pub task_type: Option<f64>,
pub task_health: Option<f64>,
pub user_states: Option<f64>,
}
impl ConfidenceScores {
pub fn new() -> Self {
Self {
task_type: None,
task_health: None,
user_states: None,
}
}
pub fn with_task_type(mut self, score: f64) -> Self {
self.task_type = Some(score.clamp(0.0, 1.0));
self
}
pub fn with_task_health(mut self, score: f64) -> Self {
self.task_health = Some(score.clamp(0.0, 1.0));
self
}
pub fn with_user_states(mut self, score: f64) -> Self {
self.user_states = Some(score.clamp(0.0, 1.0));
self
}
pub fn merge(self, other: ConfidenceScores) -> Self {
Self {
task_type: Self::max_optional(self.task_type, other.task_type),
task_health: Self::max_optional(self.task_health, other.task_health),
user_states: Self::max_optional(self.user_states, other.user_states),
}
}
fn max_optional(a: Option<f64>, b: Option<f64>) -> Option<f64> {
match (a, b) {
(Some(x), Some(y)) => Some(x.max(y)),
(Some(x), None) => Some(x),
(None, Some(y)) => Some(y),
(None, None) => None,
}
}
}
impl Default for ConfidenceScores {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detected_context_builder() {
let ctx = DetectedContext::new()
.with_task_type("security-review")
.with_task_health(TaskHealth::AtRisk)
.with_user_state("beginner")
.detected_by("RuleBasedDetector");
assert_eq!(ctx.task_type, Some("security-review".to_string()));
assert_eq!(ctx.task_health, Some(TaskHealth::AtRisk));
assert_eq!(ctx.user_states, vec!["beginner"]);
assert_eq!(ctx.detected_by, vec!["RuleBasedDetector"]);
}
#[test]
fn test_detected_context_merge() {
let ctx1 = DetectedContext::new()
.with_task_type("debug")
.with_user_state("beginner")
.detected_by("Layer1");
let ctx2 = DetectedContext::new()
.with_task_health(TaskHealth::AtRisk)
.with_user_state("confused")
.detected_by("Layer2");
let merged = ctx1.merge(ctx2);
assert_eq!(merged.task_type, Some("debug".to_string()));
assert_eq!(merged.task_health, Some(TaskHealth::AtRisk));
assert_eq!(merged.user_states, vec!["beginner", "confused"]);
assert_eq!(merged.detected_by, vec!["Layer1", "Layer2"]);
}
#[test]
fn test_detected_context_merge_dedup_user_states() {
let ctx1 = DetectedContext::new()
.with_user_state("beginner")
.with_user_state("confused");
let ctx2 = DetectedContext::new()
.with_user_state("beginner") .with_user_state("expert");
let merged = ctx1.merge(ctx2);
assert_eq!(merged.user_states, vec!["beginner", "confused", "expert"]);
}
#[test]
fn test_confidence_scores() {
let conf = ConfidenceScores::new()
.with_task_type(0.9)
.with_task_health(0.8);
assert_eq!(conf.task_type, Some(0.9));
assert_eq!(conf.task_health, Some(0.8));
assert_eq!(conf.user_states, None);
}
#[test]
fn test_confidence_scores_merge() {
let conf1 = ConfidenceScores::new()
.with_task_type(0.7)
.with_task_health(0.5);
let conf2 = ConfidenceScores::new()
.with_task_type(0.9)
.with_user_states(0.8);
let merged = conf1.merge(conf2);
assert_eq!(merged.task_type, Some(0.9)); assert_eq!(merged.task_health, Some(0.5));
assert_eq!(merged.user_states, Some(0.8));
}
#[test]
fn test_confidence_scores_clamp() {
let conf = ConfidenceScores::new()
.with_task_type(1.5) .with_task_health(-0.2);
assert_eq!(conf.task_type, Some(1.0));
assert_eq!(conf.task_health, Some(0.0));
}
}