cortex_runtime/cli/
temporal_cmd.rs1use 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
29pub 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
65pub 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}