cortex_runtime/cli/
query_cmd.rs1use crate::cli::output;
4use crate::intelligence::cache::MapCache;
5use crate::map::types::{FeatureRange, NodeQuery, PageType, FEAT_PRICE, FEAT_RATING};
6use anyhow::{bail, Result};
7
8pub async fn run(
10 domain: &str,
11 page_type: Option<&str>,
12 price_lt: Option<f32>,
13 rating_gt: Option<f32>,
14 limit: u32,
15 feature_filters: &[String],
16) -> Result<()> {
17 let mut cache = MapCache::default_cache()?;
19 let map = match cache.load_map(domain)? {
20 Some(m) => m,
21 None => {
22 if output::is_json() {
23 output::print_json(&serde_json::json!({
24 "error": "no_map",
25 "message": format!("No cached map for '{domain}'"),
26 "hint": format!("Run: cortex map {domain}")
27 }));
28 return Ok(());
29 }
30 bail!("No map found for '{domain}'. Run 'cortex map {domain}' first.");
31 }
32 };
33
34 let mut feature_ranges = Vec::new();
36
37 let page_types = page_type.map(|t| {
38 let pt = parse_page_type(t);
39 if matches!(pt, PageType::Unknown) && !output::is_quiet() {
40 eprintln!(
41 " Warning: unknown page type '{t}'. Did you mean one of: home, product_detail, article, search_results, login?"
42 );
43 }
44 vec![pt]
45 });
46
47 if let Some(price) = price_lt {
48 feature_ranges.push(FeatureRange {
49 dimension: FEAT_PRICE,
50 min: None,
51 max: Some(price),
52 });
53 }
54
55 if let Some(rating) = rating_gt {
56 feature_ranges.push(FeatureRange {
57 dimension: FEAT_RATING,
58 min: Some(rating),
59 max: None,
60 });
61 }
62
63 for f in feature_filters {
65 if let Some(range) = parse_feature_filter(f) {
66 feature_ranges.push(range);
67 } else if !output::is_quiet() {
68 eprintln!(" Warning: could not parse feature filter '{f}'. Use format: \"48<300\" or \"52>0.8\"");
69 }
70 }
71
72 let query = NodeQuery {
73 page_types,
74 feature_ranges,
75 limit: limit as usize,
76 ..Default::default()
77 };
78
79 let results = map.filter(&query);
80
81 if output::is_json() {
82 let items: Vec<serde_json::Value> = results
83 .iter()
84 .map(|m| {
85 serde_json::json!({
86 "index": m.index,
87 "url": m.url,
88 "page_type": format!("{:?}", m.page_type),
89 "confidence": m.confidence,
90 })
91 })
92 .collect();
93 output::print_json(&serde_json::json!({
94 "domain": domain,
95 "total": results.len(),
96 "results": items,
97 }));
98 return Ok(());
99 }
100
101 if results.is_empty() {
102 if !output::is_quiet() {
103 eprintln!(" No matching pages found. Try broader filters.");
104 }
105 return Ok(());
106 }
107
108 if !output::is_quiet() {
109 let total = results.len();
110 if total > limit as usize {
111 eprintln!(
112 " Found {} matching pages. Showing first {} (use --limit to change).",
113 total, limit
114 );
115 } else {
116 eprintln!(" Found {} matching page(s):", total);
117 }
118 eprintln!();
119
120 for m in &results {
121 let truncated_url = if m.url.len() > 50 {
122 format!("{}...", &m.url[..47])
123 } else {
124 m.url.clone()
125 };
126 eprintln!(
127 " [{:>5}] {:<20} {:<50} conf: {:.2}",
128 m.index,
129 format!("{:?}", m.page_type),
130 truncated_url,
131 m.confidence,
132 );
133 }
134 }
135
136 Ok(())
137}
138
139fn parse_page_type(s: &str) -> PageType {
141 if let Some(hex_str) = s.strip_prefix("0x") {
143 if let Ok(n) = u8::from_str_radix(hex_str, 16) {
144 return PageType::from_u8(n);
145 }
146 }
147
148 if let Ok(n) = s.parse::<u8>() {
150 return PageType::from_u8(n);
151 }
152
153 match s.to_lowercase().as_str() {
155 "home" => PageType::Home,
156 "product" | "product_detail" => PageType::ProductDetail,
157 "product_listing" | "listing" | "category" => PageType::ProductListing,
158 "article" | "blog" | "post" => PageType::Article,
159 "search" | "search_results" => PageType::SearchResults,
160 "login" | "signin" => PageType::Login,
161 "cart" | "basket" => PageType::Cart,
162 "checkout" => PageType::Checkout,
163 "account" | "profile" => PageType::Account,
164 "docs" | "documentation" => PageType::Documentation,
165 "form" | "form_page" => PageType::FormPage,
166 "about" | "about_page" => PageType::AboutPage,
167 "contact" | "contact_page" => PageType::ContactPage,
168 "faq" => PageType::Faq,
169 "pricing" | "pricing_page" => PageType::PricingPage,
170 _ => PageType::Unknown,
171 }
172}
173
174fn parse_feature_filter(s: &str) -> Option<FeatureRange> {
176 if let Some(pos) = s.find('<') {
178 let dim: usize = s[..pos].trim().parse().ok()?;
179 let val: f32 = s[pos + 1..].trim().parse().ok()?;
180 if dim > 127 {
181 return None;
182 }
183 return Some(FeatureRange {
184 dimension: dim,
185 min: None,
186 max: Some(val),
187 });
188 }
189
190 if let Some(pos) = s.find('>') {
192 let dim: usize = s[..pos].trim().parse().ok()?;
193 let val: f32 = s[pos + 1..].trim().parse().ok()?;
194 if dim > 127 {
195 return None;
196 }
197 return Some(FeatureRange {
198 dimension: dim,
199 min: Some(val),
200 max: None,
201 });
202 }
203
204 None
205}