1use 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#[derive(Debug, Clone)]
17pub struct QueryOptions {
18 pub cache: PathBuf,
20 pub json: bool,
22 pub source: Option<SourceOrigin>,
24 pub confidence: Option<String>,
26 pub needs_review: bool,
28}
29
30#[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 Provenance,
53}
54
55pub 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 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#[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 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 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
328fn 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 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 println!();
393 println!("{}:", style("Review Status").bold());
394 println!(" Needs review: {}", stats.summary.needs_review);
395 println!(" Reviewed: {}", stats.summary.reviewed);
396
397 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 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 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
441fn 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}