acp/commands/
query.rs

1//! @acp:module "Query Command"
2//! @acp:summary "Query the cache for symbols, files, and domains"
3//! @acp:domain cli
4//! @acp:layer handler
5
6use std::path::PathBuf;
7
8use anyhow::{anyhow, Result};
9use console::style;
10
11use crate::cache::Cache;
12use crate::parse::SourceOrigin;
13use crate::query::Query;
14
15/// Options for the query command
16#[derive(Debug, Clone)]
17pub struct QueryOptions {
18    /// Cache file to query
19    pub cache: PathBuf,
20    /// Output as JSON
21    pub json: bool,
22    /// RFC-0003: Filter by source origin
23    pub source: Option<SourceOrigin>,
24    /// RFC-0003: Filter by confidence expression (e.g., "<0.7", ">=0.9")
25    pub confidence: Option<String>,
26    /// RFC-0003: Show only annotations needing review
27    pub needs_review: bool,
28}
29
30/// Query subcommand types
31#[derive(Debug, Clone)]
32pub enum QuerySubcommand {
33    Symbol {
34        name: String,
35    },
36    File {
37        path: String,
38    },
39    Callers {
40        symbol: String,
41    },
42    Callees {
43        symbol: String,
44    },
45    Domains,
46    Domain {
47        name: String,
48    },
49    Hotpaths,
50    Stats,
51    /// RFC-0003: Show provenance statistics
52    Provenance,
53}
54
55/// Execute the query command
56pub fn execute_query(options: QueryOptions, subcommand: QuerySubcommand) -> Result<()> {
57    let cache_data = Cache::from_json(&options.cache)?;
58    let q = Query::new(&cache_data);
59
60    match subcommand {
61        QuerySubcommand::Symbol { name } => query_symbol(&q, &name, options.json),
62        QuerySubcommand::File { path } => query_file(&q, &cache_data, &path, options.json),
63        QuerySubcommand::Callers { symbol } => query_callers(&q, &symbol, options.json),
64        QuerySubcommand::Callees { symbol } => query_callees(&q, &symbol, options.json),
65        QuerySubcommand::Domains => query_domains(&q, options.json),
66        QuerySubcommand::Domain { name } => query_domain(&q, &name),
67        QuerySubcommand::Hotpaths => query_hotpaths(&q),
68        QuerySubcommand::Stats => query_stats(&cache_data, options.json),
69        QuerySubcommand::Provenance => query_provenance(&cache_data, &options),
70    }
71}
72
73fn query_symbol(q: &Query, name: &str, json: bool) -> Result<()> {
74    if let Some(sym) = q.symbol(name) {
75        if json {
76            println!("{}", serde_json::to_string_pretty(sym)?);
77        } else {
78            println!("{}", style(&sym.name).bold());
79            println!("{}", "=".repeat(60));
80            println!();
81
82            // Location
83            if sym.lines.len() >= 2 {
84                println!("Location: {}:{}-{}", sym.file, sym.lines[0], sym.lines[1]);
85            } else if !sym.lines.is_empty() {
86                println!("Location: {}:{}", sym.file, sym.lines[0]);
87            } else {
88                println!("Location: {}", sym.file);
89            }
90
91            println!("Type:     {:?}", sym.symbol_type);
92
93            if let Some(ref purpose) = sym.purpose {
94                println!("Purpose:  {}", purpose);
95            }
96
97            if let Some(ref constraints) = sym.constraints {
98                println!();
99                println!("{}:", style("Constraints").bold());
100                println!(
101                    "  @acp:lock {} - {}",
102                    constraints.level, &constraints.directive
103                );
104            }
105
106            if let Some(ref sig) = sym.signature {
107                println!();
108                println!("{}:", style("Signature").bold());
109                println!("  {}", sig);
110            }
111
112            let callers = q.callers(name);
113            if !callers.is_empty() {
114                println!();
115                println!("{} ({}):", style("Callers").bold(), callers.len());
116                println!("  {}", callers.join(", "));
117            }
118        }
119    } else {
120        eprintln!("{} Symbol not found: {}", style("✗").red(), name);
121    }
122    Ok(())
123}
124
125fn query_file(q: &Query, cache_data: &Cache, path: &str, json: bool) -> Result<()> {
126    if let Some(file) = q.file(path) {
127        if json {
128            println!("{}", serde_json::to_string_pretty(file)?);
129        } else {
130            println!("{}", style(&file.path).bold());
131            println!("{}", "=".repeat(60));
132            println!();
133
134            println!("{}:", style("File Metadata").bold());
135
136            if let Some(ref purpose) = file.purpose {
137                println!("  Purpose:     {}", purpose);
138            }
139
140            println!("  Lines:       {}", file.lines);
141            println!("  Language:    {:?}", file.language);
142
143            if let Some(ref constraints) = cache_data.constraints {
144                if let Some(fc) = constraints.by_file.get(&file.path) {
145                    if let Some(ref mutation) = fc.mutation {
146                        println!("  Constraint:  {:?}", mutation.level);
147                    }
148                }
149            }
150
151            if !file.exports.is_empty() {
152                println!();
153                println!("{}:", style("Symbols").bold());
154                for sym_name in &file.exports {
155                    if let Some(sym) = cache_data.symbols.get(sym_name) {
156                        let sym_type = format!("{:?}", sym.symbol_type).to_lowercase();
157                        let line_info = if sym.lines.len() >= 2 {
158                            format!("{}:{}-{}", sym_type, sym.lines[0], sym.lines[1])
159                        } else if !sym.lines.is_empty() {
160                            format!("{}:{}", sym_type, sym.lines[0])
161                        } else {
162                            sym_type
163                        };
164
165                        let frozen = if sym
166                            .constraints
167                            .as_ref()
168                            .map(|c| c.level == "frozen")
169                            .unwrap_or(false)
170                        {
171                            " [frozen]"
172                        } else {
173                            ""
174                        };
175                        println!("  {} ({}){}", sym.name, line_info, frozen);
176                    } else {
177                        println!("  {}", sym_name);
178                    }
179                }
180            }
181
182            if !file.inline.is_empty() {
183                println!();
184                println!("{}:", style("Inline Annotations").bold());
185                for ann in &file.inline {
186                    let expires = ann
187                        .expires
188                        .as_ref()
189                        .map(|e| format!(" (expires {})", e))
190                        .unwrap_or_default();
191                    println!(
192                        "  Line {}: @acp:{} - {}{}",
193                        ann.line, ann.annotation_type, ann.directive, expires
194                    );
195                }
196            }
197        }
198    } else {
199        eprintln!("{} File not found: {}", style("✗").red(), path);
200    }
201    Ok(())
202}
203
204fn query_callers(q: &Query, symbol: &str, json: bool) -> Result<()> {
205    let callers = q.callers(symbol);
206    if callers.is_empty() {
207        println!("{} No callers found for {}", style("ℹ").cyan(), symbol);
208    } else if json {
209        println!("{}", serde_json::to_string_pretty(&callers)?);
210    } else {
211        for caller in callers {
212            println!("{}", caller);
213        }
214    }
215    Ok(())
216}
217
218fn query_callees(q: &Query, symbol: &str, json: bool) -> Result<()> {
219    let callees = q.callees(symbol);
220    if callees.is_empty() {
221        println!("{} No callees found for {}", style("ℹ").cyan(), symbol);
222    } else if json {
223        println!("{}", serde_json::to_string_pretty(&callees)?);
224    } else {
225        for callee in callees {
226            println!("{}", callee);
227        }
228    }
229    Ok(())
230}
231
232fn query_domains(q: &Query, json: bool) -> Result<()> {
233    let domains: Vec<_> = q.domains().collect();
234    if json {
235        println!("{}", serde_json::to_string_pretty(&domains)?);
236    } else {
237        for domain in &domains {
238            println!(
239                "{}: {} files, {} symbols",
240                style(&domain.name).cyan(),
241                domain.files.len(),
242                domain.symbols.len()
243            );
244        }
245    }
246    Ok(())
247}
248
249fn query_domain(q: &Query, name: &str) -> Result<()> {
250    if let Some(domain) = q.domain(name) {
251        println!("{}", serde_json::to_string_pretty(domain)?);
252    } else {
253        eprintln!("{} Domain not found: {}", style("✗").red(), name);
254    }
255    Ok(())
256}
257
258fn query_hotpaths(q: &Query) -> Result<()> {
259    for hp in q.hotpaths() {
260        println!("{}", hp);
261    }
262    Ok(())
263}
264
265fn query_stats(cache_data: &Cache, json: bool) -> Result<()> {
266    if json {
267        println!("{}", serde_json::to_string_pretty(&cache_data.stats)?);
268    } else {
269        println!("Files: {}", cache_data.stats.files);
270        println!("Symbols: {}", cache_data.stats.symbols);
271        println!("Lines: {}", cache_data.stats.lines);
272        println!("Coverage: {:.1}%", cache_data.stats.annotation_coverage);
273        println!("Domains: {}", cache_data.domains.len());
274    }
275    Ok(())
276}
277
278// =============================================================================
279// RFC-0003: Provenance Query Support
280// =============================================================================
281
282/// Confidence filter expression (RFC-0003)
283#[derive(Debug, Clone)]
284pub enum ConfidenceFilter {
285    Less(f64),
286    LessOrEqual(f64),
287    Greater(f64),
288    GreaterOrEqual(f64),
289    Equal(f64),
290}
291
292impl ConfidenceFilter {
293    /// Parse a confidence filter expression (e.g., "<0.7", ">=0.9")
294    pub fn parse(expr: &str) -> Result<Self> {
295        let expr = expr.trim();
296
297        if let Some(val) = expr.strip_prefix("<=") {
298            return Ok(Self::LessOrEqual(val.parse()?));
299        }
300        if let Some(val) = expr.strip_prefix(">=") {
301            return Ok(Self::GreaterOrEqual(val.parse()?));
302        }
303        if let Some(val) = expr.strip_prefix('<') {
304            return Ok(Self::Less(val.parse()?));
305        }
306        if let Some(val) = expr.strip_prefix('>') {
307            return Ok(Self::Greater(val.parse()?));
308        }
309        if let Some(val) = expr.strip_prefix('=') {
310            return Ok(Self::Equal(val.parse()?));
311        }
312
313        Err(anyhow!("Invalid confidence filter: {}", expr))
314    }
315
316    /// Check if a confidence value matches this filter
317    pub fn matches(&self, confidence: f64) -> bool {
318        match self {
319            Self::Less(v) => confidence < *v,
320            Self::LessOrEqual(v) => confidence <= *v,
321            Self::Greater(v) => confidence > *v,
322            Self::GreaterOrEqual(v) => confidence >= *v,
323            Self::Equal(v) => (confidence - v).abs() < 0.001,
324        }
325    }
326}
327
328/// Display provenance statistics dashboard (RFC-0003)
329fn query_provenance(cache_data: &Cache, options: &QueryOptions) -> Result<()> {
330    let stats = &cache_data.provenance;
331
332    if options.json {
333        println!("{}", serde_json::to_string_pretty(stats)?);
334        return Ok(());
335    }
336
337    println!("{}", style("Annotation Provenance Statistics").bold());
338    println!("{}", "=".repeat(40));
339    println!();
340
341    if stats.summary.total == 0 {
342        println!("{} No provenance data tracked yet.", style("ℹ").cyan());
343        println!();
344        println!("Run `acp index` to index your codebase with provenance tracking,");
345        println!("or `acp annotate` to generate annotations with provenance markers.");
346        return Ok(());
347    }
348
349    println!("Total annotations tracked: {}", stats.summary.total);
350    println!();
351
352    // By source breakdown
353    println!("{}:", style("By Source").bold());
354    let total = stats.summary.total as f64;
355    if stats.summary.by_source.explicit > 0 {
356        println!(
357            "  explicit:  {:>5} ({:.1}%)",
358            stats.summary.by_source.explicit,
359            (stats.summary.by_source.explicit as f64 / total) * 100.0
360        );
361    }
362    if stats.summary.by_source.converted > 0 {
363        println!(
364            "  converted: {:>5} ({:.1}%)",
365            stats.summary.by_source.converted,
366            (stats.summary.by_source.converted as f64 / total) * 100.0
367        );
368    }
369    if stats.summary.by_source.heuristic > 0 {
370        println!(
371            "  heuristic: {:>5} ({:.1}%)",
372            stats.summary.by_source.heuristic,
373            (stats.summary.by_source.heuristic as f64 / total) * 100.0
374        );
375    }
376    if stats.summary.by_source.refined > 0 {
377        println!(
378            "  refined:   {:>5} ({:.1}%)",
379            stats.summary.by_source.refined,
380            (stats.summary.by_source.refined as f64 / total) * 100.0
381        );
382    }
383    if stats.summary.by_source.inferred > 0 {
384        println!(
385            "  inferred:  {:>5} ({:.1}%)",
386            stats.summary.by_source.inferred,
387            (stats.summary.by_source.inferred as f64 / total) * 100.0
388        );
389    }
390
391    // Review status
392    println!();
393    println!("{}:", style("Review Status").bold());
394    println!("  Needs review: {}", stats.summary.needs_review);
395    println!("  Reviewed:     {}", stats.summary.reviewed);
396
397    // Average confidence per source
398    if !stats.summary.average_confidence.is_empty() {
399        println!();
400        println!("{}:", style("Average Confidence").bold());
401        for (source, avg) in &stats.summary.average_confidence {
402            println!("  {}: {:.2}", source, avg);
403        }
404    }
405
406    // Low confidence annotations
407    if !stats.low_confidence.is_empty() {
408        println!();
409        println!(
410            "{} ({}):",
411            style("Low Confidence Annotations").bold(),
412            stats.low_confidence.len()
413        );
414        for entry in stats.low_confidence.iter().take(10) {
415            println!(
416                "  {} [{}]: \"{}\" ({:.2})",
417                style(&entry.target).cyan(),
418                entry.annotation,
419                truncate_value(&entry.value, 30),
420                entry.confidence
421            );
422        }
423        if stats.low_confidence.len() > 10 {
424            println!("  ... and {} more", stats.low_confidence.len() - 10);
425        }
426    }
427
428    // Last generation info
429    if let Some(ref gen) = stats.last_generation {
430        println!();
431        println!("{}:", style("Last Generation").bold());
432        println!("  ID:        {}", gen.id);
433        println!("  Timestamp: {}", gen.timestamp);
434        println!("  Generated: {} annotations", gen.annotations_generated);
435        println!("  Files:     {}", gen.files_affected);
436    }
437
438    Ok(())
439}
440
441/// Truncate a string value for display
442fn truncate_value(s: &str, max_len: usize) -> String {
443    if s.len() <= max_len {
444        s.to_string()
445    } else {
446        format!("{}...", &s[..max_len - 3])
447    }
448}