Skip to main content

cortex_runtime/cli/
temporal_cmd.rs

1//! CLI handlers for temporal commands (history, patterns).
2
3use crate::cli::output;
4use crate::collective::registry::LocalRegistry;
5use crate::temporal::patterns;
6use crate::temporal::store::TemporalStore;
7use anyhow::Result;
8use chrono::{DateTime, NaiveDateTime, Utc};
9use std::path::PathBuf;
10use std::sync::Arc;
11
12fn registry_dir() -> PathBuf {
13    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
14    home.join(".cortex").join("registry")
15}
16
17fn dim_name_to_num(name: &str) -> u8 {
18    match name {
19        "price" => 48,
20        "original_price" => 49,
21        "discount" => 50,
22        "availability" => 51,
23        "rating" => 52,
24        "review_count" => 53,
25        _ => name.parse().unwrap_or(0),
26    }
27}
28
29/// Run the history command.
30pub async fn run_history(domain: &str, url: &str, dim: &str, since: &str) -> Result<()> {
31    let registry = Arc::new(LocalRegistry::new(registry_dir())?);
32    let store = TemporalStore::new(registry);
33
34    let since_dt: DateTime<Utc> = if let Ok(dt) = DateTime::parse_from_rfc3339(since) {
35        dt.with_timezone(&Utc)
36    } else if let Ok(date) = chrono::NaiveDate::parse_from_str(since, "%Y-%m-%d") {
37        date.and_hms_opt(0, 0, 0).unwrap().and_utc()
38    } else {
39        anyhow::bail!(
40            "invalid date format: {since}. Use ISO 8601 (e.g., 2025-01-01 or 2025-01-01T00:00:00Z)"
41        );
42    };
43
44    let dim_num = dim_name_to_num(dim);
45    let points = store.history(domain, url, dim_num, since_dt)?;
46
47    if output::is_json() {
48        let json_points: Vec<serde_json::Value> = points
49            .iter()
50            .map(|(ts, v)| serde_json::json!([ts.to_rfc3339(), v]))
51            .collect();
52        output::print_json(&serde_json::json!({"points": json_points}));
53    } else if points.is_empty() {
54        println!("  No history data found.");
55    } else {
56        println!("  History for {domain} {url} dim={dim} since {since}:\n");
57        for (ts, val) in &points {
58            println!("    {}  {:.2}", ts.format("%Y-%m-%d %H:%M"), val);
59        }
60    }
61
62    Ok(())
63}
64
65/// Run the patterns command.
66pub async fn run_patterns(domain: &str, url: &str, dim: &str) -> Result<()> {
67    let registry = Arc::new(LocalRegistry::new(registry_dir())?);
68    let store = TemporalStore::new(registry);
69
70    let dim_num = dim_name_to_num(dim);
71    let since = Utc::now() - chrono::Duration::days(365);
72    let points = store.history(domain, url, dim_num, since)?;
73
74    if points.is_empty() {
75        if !output::is_quiet() {
76            println!("  No history data for pattern detection.");
77        }
78        return Ok(());
79    }
80
81    let detected = patterns::detect_patterns(&points);
82
83    if output::is_json() {
84        output::print_json(&serde_json::json!({"patterns": detected}));
85    } else if detected.is_empty() {
86        println!("  No patterns detected (need more data points).");
87    } else {
88        println!("  Detected patterns:\n");
89        for p in &detected {
90            match p {
91                patterns::Pattern::Trend {
92                    direction,
93                    slope,
94                    confidence,
95                } => {
96                    println!(
97                        "    Trend: {:?} (slope={:.3}/day, confidence={:.2})",
98                        direction, slope, confidence
99                    );
100                }
101                patterns::Pattern::Periodic {
102                    period,
103                    confidence,
104                    phase,
105                } => {
106                    let days = *period as f64 / 86400.0;
107                    println!(
108                        "    Periodic: {:.1} day cycle (phase={:.2}, confidence={:.2})",
109                        days, phase, confidence
110                    );
111                }
112                patterns::Pattern::Anomaly {
113                    timestamp,
114                    expected_value,
115                    actual_value,
116                    sigma,
117                } => {
118                    println!(
119                        "    Anomaly: at {} (expected={:.2}, actual={:.2}, {:.1}σ)",
120                        timestamp.format("%Y-%m-%d"),
121                        expected_value,
122                        actual_value,
123                        sigma
124                    );
125                }
126                patterns::Pattern::Seasonal {
127                    season,
128                    discount_pct,
129                    confidence,
130                    ..
131                } => {
132                    println!(
133                        "    Seasonal: {} ({:.0}% discount, confidence={:.2})",
134                        season,
135                        discount_pct * 100.0,
136                        confidence
137                    );
138                }
139            }
140        }
141    }
142
143    Ok(())
144}