use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineThresholds {
pub learning_soft: usize,
pub learning_hard: usize,
pub thoughts_soft: usize,
pub thoughts_hard: usize,
pub curiosity_soft: usize,
pub curiosity_hard: usize,
pub reflections_soft: usize,
pub reflections_hard: usize,
pub praxis_soft: usize,
pub praxis_hard: usize,
}
impl Default for PipelineThresholds {
fn default() -> Self {
Self {
learning_soft: 5,
learning_hard: 8,
thoughts_soft: 5,
thoughts_hard: 10,
curiosity_soft: 3,
curiosity_hard: 7,
reflections_soft: 15,
reflections_hard: 20,
praxis_soft: 5,
praxis_hard: 10,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ThresholdStatus {
Green,
Yellow,
Red,
}
impl std::fmt::Display for ThresholdStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Green => write!(f, "green"),
Self::Yellow => write!(f, "yellow"),
Self::Red => write!(f, "red"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentHealth {
pub count: usize,
pub soft: usize,
pub hard: usize,
pub status: ThresholdStatus,
}
#[derive(Debug, Clone)]
pub struct PipelineHealth {
pub learning: DocumentHealth,
pub thoughts: DocumentHealth,
pub curiosity: DocumentHealth,
pub reflections: DocumentHealth,
pub praxis: DocumentHealth,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DocumentCounts {
pub learning: usize,
pub thoughts: usize,
pub curiosity: usize,
pub reflections: usize,
pub praxis: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PipelineState {
pub last_updated: Option<String>,
pub session_count: u32,
pub sessions_without_movement: u32,
pub last_counts: DocumentCounts,
}
impl PipelineState {
pub fn update_counts(&mut self, new_counts: &DocumentCounts, now_iso: &str) {
if *new_counts == self.last_counts {
self.sessions_without_movement += 1;
} else {
self.sessions_without_movement = 0;
}
self.last_counts = new_counts.clone();
self.session_count += 1;
self.last_updated = Some(now_iso.to_string());
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CognitiveStatus {
Healthy,
Watch,
Concern,
Alert,
}
impl std::fmt::Display for CognitiveStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Healthy => write!(f, "HEALTHY"),
Self::Watch => write!(f, "WATCH"),
Self::Concern => write!(f, "CONCERN"),
Self::Alert => write!(f, "ALERT"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Trend {
Improving,
Stable,
Declining,
}
impl std::fmt::Display for Trend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Improving => write!(f, "improving"),
Self::Stable => write!(f, "stable"),
Self::Declining => write!(f, "declining"),
}
}
}
#[derive(Debug, Clone)]
pub struct CognitiveHealth {
pub status: CognitiveStatus,
pub vocabulary_trend: Trend,
pub question_trend: Trend,
pub evidence_trend: Trend,
pub progress_trend: Trend,
pub suggestions: Vec<String>,
pub sufficient_data: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalFrame {
pub timestamp: String,
pub task_id: String,
pub vocabulary_diversity: f64,
pub question_count: usize,
pub evidence_references: usize,
pub thought_progress: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutcomeRecord {
pub task_id: String,
pub timestamp: String,
pub domain: String,
pub task_type: String,
pub description: String,
pub outcome: String,
pub tokens_used: u32,
pub tool_rounds: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Confidence {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThresholdRecommendation {
pub document: String,
pub current_soft: usize,
pub current_hard: usize,
pub recommended_soft: Option<usize>,
pub recommended_hard: Option<usize>,
pub reason: String,
pub confidence: Confidence,
pub evidence_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutcomeSummary {
pub total: usize,
pub success_rate: f64,
pub domains: Vec<(String, usize, f64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalibrationReport {
pub generated_at: String,
pub recommendations: Vec<ThresholdRecommendation>,
pub sample_size: usize,
pub outcome_summary: OutcomeSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineSnapshot {
pub timestamp: String,
pub learning: usize,
pub thoughts: usize,
pub curiosity: usize,
pub reflections: usize,
pub praxis: usize,
}
pub trait PipelineMonitor: Send + Sync {
fn calculate(&self, root_dir: &Path, thresholds: &PipelineThresholds) -> PipelineHealth;
fn render_for_prompt(
&self,
health: &PipelineHealth,
sessions_frozen: u32,
freeze_threshold: u32,
) -> String;
fn counts_from_health(&self, health: &PipelineHealth) -> DocumentCounts;
fn load_state(&self, root_dir: &Path) -> PipelineState;
fn save_state(
&self,
root_dir: &Path,
state: &PipelineState,
) -> Result<(), Box<dyn std::error::Error>>;
fn check_and_archive(
&self,
root_dir: &Path,
thresholds: &PipelineThresholds,
health: &PipelineHealth,
) -> Vec<String>;
fn list_archives(
&self,
root_dir: &Path,
document: Option<&str>,
) -> Result<Vec<String>, Box<dyn std::error::Error>>;
fn archive_by_name(
&self,
root_dir: &Path,
document: &str,
) -> Result<String, Box<dyn std::error::Error>>;
}
pub trait CognitiveMonitor: Send + Sync {
fn assess(&self, root_dir: &Path, window_size: usize, min_samples: usize) -> CognitiveHealth;
fn render_for_prompt(&self, health: &CognitiveHealth) -> String;
fn extract(&self, content: &str, task_id: &str) -> SignalFrame;
fn record(
&self,
root_dir: &Path,
frame: SignalFrame,
window_size: usize,
) -> Result<(), Box<dyn std::error::Error>>;
}
pub trait OutcomeTracker: Send + Sync {
fn build_outcome(
&self,
task_id: &str,
task_name: &str,
response_text: &str,
tool_rounds: u32,
input_tokens: u32,
output_tokens: u32,
) -> OutcomeRecord;
fn record_outcome(
&self,
docs_dir: &Path,
outcome: OutcomeRecord,
max_outcomes: usize,
) -> Result<(), Box<dyn std::error::Error>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn threshold_status_display() {
assert_eq!(ThresholdStatus::Green.to_string(), "green");
assert_eq!(ThresholdStatus::Yellow.to_string(), "yellow");
assert_eq!(ThresholdStatus::Red.to_string(), "red");
}
#[test]
fn cognitive_status_display() {
assert_eq!(CognitiveStatus::Healthy.to_string(), "HEALTHY");
assert_eq!(CognitiveStatus::Watch.to_string(), "WATCH");
assert_eq!(CognitiveStatus::Concern.to_string(), "CONCERN");
assert_eq!(CognitiveStatus::Alert.to_string(), "ALERT");
}
#[test]
fn trend_display() {
assert_eq!(Trend::Improving.to_string(), "improving");
assert_eq!(Trend::Stable.to_string(), "stable");
assert_eq!(Trend::Declining.to_string(), "declining");
}
#[test]
fn pipeline_thresholds_default() {
let t = PipelineThresholds::default();
assert_eq!(t.learning_soft, 5);
assert_eq!(t.learning_hard, 8);
assert_eq!(t.curiosity_hard, 7);
}
#[test]
fn pipeline_state_update_counts_detects_freeze() {
let mut state = PipelineState::default();
let counts = DocumentCounts {
learning: 3,
thoughts: 2,
curiosity: 1,
reflections: 5,
praxis: 2,
};
state.update_counts(&counts, "2026-03-05T12:00:00Z");
assert_eq!(state.sessions_without_movement, 0);
assert_eq!(state.session_count, 1);
state.update_counts(&counts, "2026-03-05T13:00:00Z");
assert_eq!(state.sessions_without_movement, 1);
assert_eq!(state.session_count, 2);
let new_counts = DocumentCounts {
learning: 4,
..counts
};
state.update_counts(&new_counts, "2026-03-05T14:00:00Z");
assert_eq!(state.sessions_without_movement, 0);
assert_eq!(state.session_count, 3);
}
#[test]
fn document_counts_default() {
let counts = DocumentCounts::default();
assert_eq!(counts.learning, 0);
assert_eq!(counts.thoughts, 0);
}
#[test]
fn threshold_status_equality() {
assert_eq!(ThresholdStatus::Red, ThresholdStatus::Red);
assert_ne!(ThresholdStatus::Red, ThresholdStatus::Green);
}
#[test]
fn signal_frame_serializes() {
let frame = SignalFrame {
timestamp: "2026-03-05T12:00:00Z".to_string(),
task_id: "test".to_string(),
vocabulary_diversity: 0.72,
question_count: 3,
evidence_references: 5,
thought_progress: true,
};
let json = serde_json::to_string(&frame).unwrap();
let back: SignalFrame = serde_json::from_str(&json).unwrap();
assert_eq!(back.task_id, "test");
assert!((back.vocabulary_diversity - 0.72).abs() < f64::EPSILON);
}
#[test]
fn outcome_record_serializes() {
let record = OutcomeRecord {
task_id: "task-1".to_string(),
timestamp: "2026-03-05T12:00:00Z".to_string(),
domain: "research".to_string(),
task_type: "research".to_string(),
description: "Deep dive".to_string(),
outcome: "success".to_string(),
tokens_used: 1500,
tool_rounds: 3,
};
let json = serde_json::to_string(&record).unwrap();
let back: OutcomeRecord = serde_json::from_str(&json).unwrap();
assert_eq!(back.task_id, "task-1");
assert_eq!(back.tokens_used, 1500);
}
}