Skip to main content

agentic_codebase/engine/
query.rs

1//! Query executor for all 24 query types.
2//!
3//! The [`QueryEngine`] is the single entry point for running any of the 24
4//! supported queries against a [`CodeGraph`]. Each query has its own param
5//! struct and result type, all defined in this module.
6
7use std::collections::{HashMap, HashSet, VecDeque};
8
9use crate::graph::code_graph::CodeGraph;
10use crate::graph::traversal::{self, Direction, TraversalOptions};
11use crate::types::{
12    AcbError, AcbResult, CodeUnit, CodeUnitType, EdgeType, Language, Span, Visibility,
13};
14
15// ============================================================================
16// Query param / result types
17// ============================================================================
18
19/// How symbol names are matched in lookups.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum MatchMode {
22    /// Exact string equality.
23    Exact,
24    /// Case-insensitive prefix.
25    Prefix,
26    /// Substring anywhere in the name.
27    Contains,
28    /// Fuzzy (Levenshtein distance <= threshold).
29    Fuzzy,
30}
31
32/// Parameters for Query 1: Symbol Lookup.
33#[derive(Debug, Clone)]
34pub struct SymbolLookupParams {
35    /// The search string.
36    pub name: String,
37    /// Matching strategy.
38    pub mode: MatchMode,
39    /// If non-empty, restrict to these unit types.
40    pub unit_types: Vec<CodeUnitType>,
41    /// If non-empty, restrict to these languages.
42    pub languages: Vec<Language>,
43    /// Maximum results to return (0 = unlimited).
44    pub limit: usize,
45    /// Fuzzy threshold (max edit distance). Only used when mode = Fuzzy.
46    pub fuzzy_threshold: usize,
47}
48
49impl Default for SymbolLookupParams {
50    fn default() -> Self {
51        Self {
52            name: String::new(),
53            mode: MatchMode::Exact,
54            unit_types: Vec::new(),
55            languages: Vec::new(),
56            limit: 0,
57            fuzzy_threshold: 2,
58        }
59    }
60}
61
62/// Parameters for Queries 2 & 3: Dependency / Reverse-Dependency.
63#[derive(Debug, Clone)]
64pub struct DependencyParams {
65    /// Starting code unit.
66    pub unit_id: u64,
67    /// Maximum traversal depth.
68    pub max_depth: u32,
69    /// Edge types to follow (empty = all dependency types).
70    pub edge_types: Vec<EdgeType>,
71    /// Whether to include transitive (multi-hop) dependencies.
72    pub include_transitive: bool,
73}
74
75/// A single dependency node in a result tree.
76#[derive(Debug, Clone)]
77pub struct DependencyNode {
78    /// The code unit id.
79    pub unit_id: u64,
80    /// Depth from the origin.
81    pub depth: u32,
82    /// The path of unit IDs from origin to this node.
83    pub path: Vec<u64>,
84}
85
86/// Result of a dependency or reverse-dependency query.
87#[derive(Debug, Clone)]
88pub struct DependencyResult {
89    /// The origin unit.
90    pub root_id: u64,
91    /// All dependency nodes found.
92    pub nodes: Vec<DependencyNode>,
93}
94
95/// Direction of call-graph exploration.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum CallDirection {
98    /// Who calls this function.
99    Callers,
100    /// What does this function call.
101    Callees,
102    /// Both directions.
103    Both,
104}
105
106/// Parameters for Query 4: Call Graph.
107#[derive(Debug, Clone)]
108pub struct CallGraphParams {
109    /// The function unit to inspect.
110    pub unit_id: u64,
111    /// Which direction to explore.
112    pub direction: CallDirection,
113    /// Maximum depth of call chain.
114    pub max_depth: u32,
115}
116
117/// A call site in the call graph.
118#[derive(Debug, Clone)]
119pub struct CallSite {
120    /// The calling unit.
121    pub caller_id: u64,
122    /// The called unit.
123    pub callee_id: u64,
124    /// Location of the call.
125    pub span: Span,
126}
127
128/// Result of a call graph query.
129#[derive(Debug, Clone)]
130pub struct CallGraphResult {
131    /// The origin function.
132    pub root_id: u64,
133    /// All functions discovered.
134    pub nodes: Vec<(u64, u32)>,
135    /// Call sites found.
136    pub call_sites: Vec<CallSite>,
137}
138
139/// Parameters for Query 5: Type Hierarchy.
140#[derive(Debug, Clone)]
141pub struct HierarchyParams {
142    /// The type unit to inspect.
143    pub unit_id: u64,
144    /// Whether to include ancestors.
145    pub include_ancestors: bool,
146    /// Whether to include descendants.
147    pub include_descendants: bool,
148}
149
150/// A node in the type hierarchy result.
151#[derive(Debug, Clone)]
152pub struct HierarchyNode {
153    /// The unit ID.
154    pub unit_id: u64,
155    /// Relationship kind (inherits or implements).
156    pub relation: EdgeType,
157    /// Depth from the query target (positive = ancestor, negative = descendant).
158    pub depth: i32,
159}
160
161/// Result of a type hierarchy query.
162#[derive(Debug, Clone)]
163pub struct HierarchyResult {
164    /// The queried type.
165    pub root_id: u64,
166    /// All hierarchy nodes.
167    pub nodes: Vec<HierarchyNode>,
168}
169
170/// Parameters for Query 7: Pattern Match.
171#[derive(Debug, Clone)]
172pub struct PatternParams {
173    /// A simple pattern DSL string.
174    ///
175    /// Supported patterns:
176    /// - `function { calls: [A, B] }` — find functions that call A and B
177    /// - `class { inherits: Base }` — find classes that inherit from Base
178    /// - `async function` — find async functions
179    /// - `function { complexity: >N }` — find functions with complexity > N
180    pub pattern: String,
181}
182
183/// A single pattern match.
184#[derive(Debug, Clone)]
185pub struct PatternMatch {
186    /// The matched unit.
187    pub unit_id: u64,
188    /// Confidence of the match (0.0–1.0).
189    pub confidence: f32,
190    /// Which part of the pattern was matched.
191    pub matched_rule: String,
192}
193
194/// Parameters for Query 8: Semantic Search.
195#[derive(Debug, Clone)]
196pub struct SemanticParams {
197    /// Query feature vector.
198    pub query_vec: Vec<f32>,
199    /// Maximum number of results.
200    pub top_k: usize,
201    /// Optional type filter.
202    pub unit_types: Vec<CodeUnitType>,
203    /// Optional language filter.
204    pub languages: Vec<Language>,
205    /// Minimum similarity threshold (0.0–1.0).
206    pub min_similarity: f32,
207}
208
209/// A single semantic search match.
210#[derive(Debug, Clone)]
211pub struct SemanticMatch {
212    /// The matched unit.
213    pub unit_id: u64,
214    /// Cosine similarity score.
215    pub score: f32,
216}
217
218/// Parameters for Query 9: Impact Analysis.
219#[derive(Debug, Clone)]
220pub struct ImpactParams {
221    /// The unit being changed.
222    pub unit_id: u64,
223    /// Maximum depth of impact propagation.
224    pub max_depth: u32,
225    /// Edge types to consider (empty = all dependency types).
226    pub edge_types: Vec<EdgeType>,
227}
228
229/// A single impacted unit.
230#[derive(Debug, Clone)]
231pub struct ImpactedUnit {
232    /// The affected unit.
233    pub unit_id: u64,
234    /// Distance from origin.
235    pub depth: u32,
236    /// Risk score (0.0 = low, 1.0 = high).
237    pub risk_score: f32,
238    /// Whether this unit has test coverage.
239    pub has_tests: bool,
240}
241
242/// Result of an impact analysis.
243#[derive(Debug, Clone)]
244pub struct ImpactResult {
245    /// The origin unit.
246    pub root_id: u64,
247    /// All impacted units.
248    pub impacted: Vec<ImpactedUnit>,
249    /// Overall risk score.
250    pub overall_risk: f32,
251    /// Recommendations.
252    pub recommendations: Vec<String>,
253}
254
255/// Result of a test coverage query.
256#[derive(Debug, Clone)]
257pub struct CoverageResult {
258    /// The unit being queried.
259    pub unit_id: u64,
260    /// Direct tests (test units with a Tests edge to this unit).
261    pub direct_tests: Vec<u64>,
262    /// Indirect tests (tests of callers).
263    pub indirect_tests: Vec<u64>,
264    /// Estimated coverage ratio (0.0–1.0).
265    pub coverage_ratio: f32,
266}
267
268/// Parameters for Query 11: Cross-Language Trace.
269#[derive(Debug, Clone)]
270pub struct TraceParams {
271    /// Starting unit.
272    pub unit_id: u64,
273    /// Maximum hops.
274    pub max_hops: u32,
275}
276
277/// A single hop in a cross-language trace.
278#[derive(Debug, Clone)]
279pub struct TraceHop {
280    /// The unit at this hop.
281    pub unit_id: u64,
282    /// Language of this unit.
283    pub language: Language,
284    /// The edge type that led here.
285    pub via_edge: Option<EdgeType>,
286}
287
288/// Result of a cross-language trace.
289#[derive(Debug, Clone)]
290pub struct TraceResult {
291    /// All hops from origin.
292    pub hops: Vec<TraceHop>,
293    /// Languages traversed (in order).
294    pub languages_crossed: Vec<Language>,
295}
296
297/// Parameters for Query 12: Collective Patterns.
298#[derive(Debug, Clone)]
299pub struct CollectiveParams {
300    /// Optional type filter.
301    pub unit_type: Option<CodeUnitType>,
302    /// Minimum collective usage count.
303    pub min_usage: u64,
304    /// Maximum results.
305    pub limit: usize,
306}
307
308/// A single collective pattern match.
309#[derive(Debug, Clone)]
310pub struct CollectivePatternEntry {
311    /// The unit ID.
312    pub unit_id: u64,
313    /// Collective usage count.
314    pub usage_count: u64,
315    /// Confidence/relevance.
316    pub confidence: f32,
317}
318
319/// Result of a collective patterns query.
320#[derive(Debug, Clone)]
321pub struct CollectiveResult {
322    /// Pattern entries found.
323    pub patterns: Vec<CollectivePatternEntry>,
324    /// Whether collective data is available.
325    pub collective_available: bool,
326}
327
328/// Result of a temporal evolution query.
329#[derive(Debug, Clone)]
330pub struct EvolutionResult {
331    /// The queried unit.
332    pub unit_id: u64,
333    /// Number of changes.
334    pub change_count: u32,
335    /// Created-at timestamp.
336    pub created_at: u64,
337    /// Last modified timestamp.
338    pub last_modified: u64,
339    /// Stability score.
340    pub stability_score: f32,
341    /// Trend description.
342    pub trend: String,
343}
344
345/// Stability factor for a code unit.
346#[derive(Debug, Clone)]
347pub struct StabilityFactor {
348    /// Factor name.
349    pub name: String,
350    /// Value (0.0–1.0, higher = better).
351    pub value: f32,
352    /// Explanation.
353    pub description: String,
354}
355
356/// Result of a stability analysis.
357#[derive(Debug, Clone)]
358pub struct StabilityResult {
359    /// The queried unit.
360    pub unit_id: u64,
361    /// Overall stability score (0.0–1.0).
362    pub overall_score: f32,
363    /// Individual factors.
364    pub factors: Vec<StabilityFactor>,
365    /// Recommendation.
366    pub recommendation: String,
367}
368
369/// Parameters for Query 15: Coupling Detection.
370#[derive(Debug, Clone)]
371pub struct CouplingParams {
372    /// Optional unit to centre the search on. If None, scan all.
373    pub unit_id: Option<u64>,
374    /// Minimum coupling strength threshold.
375    pub min_strength: f32,
376}
377
378/// A detected coupling between two units.
379#[derive(Debug, Clone)]
380pub struct Coupling {
381    /// First unit.
382    pub unit_a: u64,
383    /// Second unit.
384    pub unit_b: u64,
385    /// Coupling strength (0.0–1.0).
386    pub strength: f32,
387    /// Kind of coupling.
388    pub kind: CouplingKind,
389}
390
391/// The kind of detected coupling.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub enum CouplingKind {
394    /// Direct edge between units.
395    Explicit,
396    /// CouplesWith temporal edge.
397    Temporal,
398    /// Share many connections.
399    Hidden,
400}
401
402/// Parameters for Query 16: Dead Code.
403#[derive(Debug, Clone)]
404pub struct DeadCodeParams {
405    /// Consider only these unit types as potential dead code (empty = all).
406    pub unit_types: Vec<CodeUnitType>,
407    /// Whether to include test files in reachability roots.
408    pub include_tests_as_roots: bool,
409}
410
411/// Parameters for Query 17: Prophecy.
412#[derive(Debug, Clone)]
413pub struct ProphecyParams {
414    /// Number of predictions to return.
415    pub top_k: usize,
416    /// Minimum risk threshold.
417    pub min_risk: f32,
418}
419
420/// A single prophecy prediction.
421#[derive(Debug, Clone)]
422pub struct Prediction {
423    /// The unit predicted to be at risk.
424    pub unit_id: u64,
425    /// Risk score (0.0–1.0).
426    pub risk_score: f32,
427    /// Reason for the prediction.
428    pub reason: String,
429}
430
431/// Result of a prophecy query.
432#[derive(Debug, Clone)]
433pub struct ProphecyResult {
434    /// Predictions sorted by risk.
435    pub predictions: Vec<Prediction>,
436}
437
438/// A concept-mapped unit and its role.
439#[derive(Debug, Clone)]
440pub struct ConceptUnit {
441    /// The unit.
442    pub unit_id: u64,
443    /// Role of this unit in the concept.
444    pub role: ConceptRole,
445    /// Relevance score.
446    pub relevance: f32,
447}
448
449/// The role a unit plays in a concept mapping.
450#[derive(Debug, Clone, PartialEq, Eq)]
451pub enum ConceptRole {
452    /// Defines the concept.
453    Definition,
454    /// Uses the concept.
455    Usage,
456    /// Extends the concept.
457    Extension,
458    /// Tests the concept.
459    Test,
460}
461
462/// Result of a concept mapping query.
463#[derive(Debug, Clone)]
464pub struct ConceptMap {
465    /// The queried concept.
466    pub concept: String,
467    /// Units related to this concept.
468    pub units: Vec<ConceptUnit>,
469}
470
471/// Parameters for Query 19: Migration Path.
472#[derive(Debug, Clone)]
473pub struct MigrationParams {
474    /// The unit to migrate from.
475    pub from_unit: u64,
476    /// The unit to migrate to.
477    pub to_unit: u64,
478}
479
480/// Safety level of a migration step.
481#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
482pub enum SafetyLevel {
483    /// Safe — has tests.
484    Safe,
485    /// Caution — no direct tests but has callers with tests.
486    Caution,
487    /// Risky — no test coverage at all.
488    Risky,
489}
490
491/// A single step in a migration plan.
492#[derive(Debug, Clone)]
493pub struct MigrationStep {
494    /// The unit that needs updating.
495    pub unit_id: u64,
496    /// Order in which to do the migration.
497    pub order: u32,
498    /// Safety level.
499    pub safety: SafetyLevel,
500    /// Description of what to do.
501    pub description: String,
502}
503
504/// Result of a migration path query.
505#[derive(Debug, Clone)]
506pub struct MigrationPlan {
507    /// Source unit.
508    pub from_unit: u64,
509    /// Target unit.
510    pub to_unit: u64,
511    /// Ordered steps.
512    pub steps: Vec<MigrationStep>,
513}
514
515/// Parameters for Query 20: Test Gap.
516#[derive(Debug, Clone)]
517pub struct TestGapParams {
518    /// Minimum change count to consider.
519    pub min_changes: u32,
520    /// Minimum complexity to consider.
521    pub min_complexity: u32,
522    /// Unit types to scan (empty = Function only).
523    pub unit_types: Vec<CodeUnitType>,
524}
525
526/// A detected test gap.
527#[derive(Debug, Clone)]
528pub struct TestGap {
529    /// The unit missing tests.
530    pub unit_id: u64,
531    /// Why this is flagged.
532    pub reason: String,
533    /// Priority score (higher = more urgent).
534    pub priority: f32,
535}
536
537/// Parameters for Query 21: Architectural Drift.
538#[derive(Debug, Clone)]
539pub struct DriftParams {
540    /// Architectural rules to check.
541    pub rules: Vec<ArchRule>,
542}
543
544/// A single architectural rule.
545#[derive(Debug, Clone)]
546pub enum ArchRule {
547    /// Layer A must not depend on Layer B.
548    LayerDependency {
549        /// Upper layer module prefix.
550        upper: String,
551        /// Lower layer module prefix.
552        lower: String,
553    },
554    /// Module must not have external edges to another module.
555    ModuleBoundary {
556        /// Module prefix.
557        module: String,
558    },
559    /// All units matching a prefix must follow a naming convention.
560    NamingConvention {
561        /// Module/path prefix.
562        prefix: String,
563        /// Regex pattern that names must match.
564        pattern: String,
565    },
566    /// No dependency cycles in the given scope.
567    Cyclic {
568        /// Module prefix scope.
569        scope: String,
570    },
571}
572
573/// A single drift violation.
574#[derive(Debug, Clone)]
575pub struct DriftViolation {
576    /// Which rule was violated.
577    pub rule_index: usize,
578    /// Description of the violation.
579    pub description: String,
580    /// Units involved.
581    pub units: Vec<u64>,
582}
583
584/// Result of an architectural drift check.
585#[derive(Debug, Clone)]
586pub struct DriftReport {
587    /// All violations found.
588    pub violations: Vec<DriftViolation>,
589    /// Overall conformance score (1.0 = no drift).
590    pub conformance_score: f32,
591}
592
593/// Parameters for Query 22: Similarity.
594#[derive(Debug, Clone)]
595pub struct SimilarityParams {
596    /// The unit to compare against.
597    pub unit_id: u64,
598    /// Number of results.
599    pub top_k: usize,
600    /// Minimum similarity.
601    pub min_similarity: f32,
602}
603
604/// A single similarity match.
605#[derive(Debug, Clone)]
606pub struct SimilarityMatch {
607    /// The similar unit.
608    pub unit_id: u64,
609    /// Cosine similarity score.
610    pub score: f32,
611}
612
613/// Result of a shortest-path query.
614#[derive(Debug, Clone)]
615pub struct PathResult {
616    /// Whether a path was found.
617    pub found: bool,
618    /// The path of unit IDs from source to destination.
619    pub path: Vec<u64>,
620    /// Edge types along the path.
621    pub edge_types: Vec<EdgeType>,
622    /// Total path length.
623    pub length: usize,
624}
625
626/// Parameters for Query 24: Hotspot Detection.
627#[derive(Debug, Clone)]
628pub struct HotspotParams {
629    /// Maximum results.
630    pub top_k: usize,
631    /// Minimum hotspot score threshold.
632    pub min_score: f32,
633    /// Unit types to consider (empty = all).
634    pub unit_types: Vec<CodeUnitType>,
635}
636
637/// A detected hotspot.
638#[derive(Debug, Clone)]
639pub struct Hotspot {
640    /// The unit.
641    pub unit_id: u64,
642    /// Hotspot score (higher = more concerning).
643    pub score: f32,
644    /// Contributing factors.
645    pub factors: HashMap<String, f32>,
646}
647
648// ============================================================================
649// Utility: Levenshtein distance
650// ============================================================================
651
652/// Compute the Levenshtein (edit) distance between two strings.
653fn levenshtein(a: &str, b: &str) -> usize {
654    let a_len = a.len();
655    let b_len = b.len();
656
657    if a_len == 0 {
658        return b_len;
659    }
660    if b_len == 0 {
661        return a_len;
662    }
663
664    let a_bytes = a.as_bytes();
665    let b_bytes = b.as_bytes();
666
667    // Only need two rows.
668    let mut prev = vec![0usize; b_len + 1];
669    let mut curr = vec![0usize; b_len + 1];
670
671    for (j, item) in prev.iter_mut().enumerate().take(b_len + 1) {
672        *item = j;
673    }
674
675    for i in 1..=a_len {
676        curr[0] = i;
677        for j in 1..=b_len {
678            let cost = if a_bytes[i - 1] == b_bytes[j - 1] {
679                0
680            } else {
681                1
682            };
683            curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
684        }
685        std::mem::swap(&mut prev, &mut curr);
686    }
687
688    prev[b_len]
689}
690
691/// Compute cosine similarity between two f32 slices.
692fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
693    if a.len() != b.len() || a.is_empty() {
694        return 0.0;
695    }
696
697    let mut dot = 0.0_f64;
698    let mut norm_a = 0.0_f64;
699    let mut norm_b = 0.0_f64;
700
701    for (x, y) in a.iter().zip(b.iter()) {
702        let xf = *x as f64;
703        let yf = *y as f64;
704        dot += xf * yf;
705        norm_a += xf * xf;
706        norm_b += yf * yf;
707    }
708
709    let denom = norm_a.sqrt() * norm_b.sqrt();
710    if denom < 1e-12 {
711        return 0.0;
712    }
713
714    (dot / denom) as f32
715}
716
717// ============================================================================
718// QueryEngine
719// ============================================================================
720
721/// The central query executor.
722///
723/// Implements all 24 query types defined in the AgenticCodebase specification.
724/// Each method takes a shared reference to a [`CodeGraph`] and query-specific
725/// parameters, returning a strongly-typed result or an [`AcbError`].
726#[derive(Debug, Clone)]
727pub struct QueryEngine;
728
729impl QueryEngine {
730    /// Create a new query engine.
731    pub fn new() -> Self {
732        Self
733    }
734
735    // ========================================================================
736    // Query 1: Symbol Lookup
737    // ========================================================================
738
739    /// Look up symbols by name with optional filters.
740    pub fn symbol_lookup<'g>(
741        &self,
742        graph: &'g CodeGraph,
743        params: SymbolLookupParams,
744    ) -> AcbResult<Vec<&'g CodeUnit>> {
745        let candidates: Vec<&CodeUnit> = match params.mode {
746            MatchMode::Exact => graph.find_units_by_exact_name(&params.name),
747            MatchMode::Prefix => graph.find_units_by_name(&params.name),
748            MatchMode::Contains => {
749                let lower = params.name.to_lowercase();
750                graph
751                    .units()
752                    .iter()
753                    .filter(|u| u.name.to_lowercase().contains(&lower))
754                    .collect()
755            }
756            MatchMode::Fuzzy => {
757                let lower = params.name.to_lowercase();
758                let threshold = params.fuzzy_threshold;
759                graph
760                    .units()
761                    .iter()
762                    .filter(|u| levenshtein(&u.name.to_lowercase(), &lower) <= threshold)
763                    .collect()
764            }
765        };
766
767        // Apply type filter.
768        let filtered: Vec<&CodeUnit> = candidates
769            .into_iter()
770            .filter(|u| params.unit_types.is_empty() || params.unit_types.contains(&u.unit_type))
771            .filter(|u| params.languages.is_empty() || params.languages.contains(&u.language))
772            .collect();
773
774        // Apply limit.
775        let result = if params.limit > 0 {
776            filtered.into_iter().take(params.limit).collect()
777        } else {
778            filtered
779        };
780
781        Ok(result)
782    }
783
784    // ========================================================================
785    // Query 2: Dependency Graph
786    // ========================================================================
787
788    /// Build the forward dependency graph from a unit.
789    pub fn dependency_graph(
790        &self,
791        graph: &CodeGraph,
792        params: DependencyParams,
793    ) -> AcbResult<DependencyResult> {
794        self.validate_unit(graph, params.unit_id)?;
795
796        let effective_depth = if params.include_transitive {
797            params.max_depth
798        } else {
799            1
800        };
801
802        let edge_types = if params.edge_types.is_empty() {
803            vec![
804                EdgeType::Calls,
805                EdgeType::Imports,
806                EdgeType::Inherits,
807                EdgeType::Implements,
808                EdgeType::UsesType,
809                EdgeType::FfiBinds,
810            ]
811        } else {
812            params.edge_types
813        };
814
815        // BFS with path tracking.
816        let mut nodes = Vec::new();
817        let mut visited = HashSet::new();
818        let mut queue = VecDeque::new();
819        visited.insert(params.unit_id);
820        queue.push_back((params.unit_id, 0u32, vec![params.unit_id]));
821
822        while let Some((current, depth, path)) = queue.pop_front() {
823            if current != params.unit_id {
824                nodes.push(DependencyNode {
825                    unit_id: current,
826                    depth,
827                    path: path.clone(),
828                });
829            }
830
831            if depth >= effective_depth {
832                continue;
833            }
834
835            for edge in graph.edges_from(current) {
836                if !edge_types.contains(&edge.edge_type) {
837                    continue;
838                }
839                if visited.insert(edge.target_id) {
840                    let mut new_path = path.clone();
841                    new_path.push(edge.target_id);
842                    queue.push_back((edge.target_id, depth + 1, new_path));
843                }
844            }
845        }
846
847        Ok(DependencyResult {
848            root_id: params.unit_id,
849            nodes,
850        })
851    }
852
853    // ========================================================================
854    // Query 3: Reverse Dependency
855    // ========================================================================
856
857    /// Build the reverse dependency graph (who depends on this unit).
858    pub fn reverse_dependency(
859        &self,
860        graph: &CodeGraph,
861        params: DependencyParams,
862    ) -> AcbResult<DependencyResult> {
863        self.validate_unit(graph, params.unit_id)?;
864
865        let effective_depth = if params.include_transitive {
866            params.max_depth
867        } else {
868            1
869        };
870
871        let edge_types = if params.edge_types.is_empty() {
872            vec![
873                EdgeType::Calls,
874                EdgeType::Imports,
875                EdgeType::Inherits,
876                EdgeType::Implements,
877                EdgeType::UsesType,
878                EdgeType::FfiBinds,
879            ]
880        } else {
881            params.edge_types
882        };
883
884        let mut nodes = Vec::new();
885        let mut visited = HashSet::new();
886        let mut queue = VecDeque::new();
887        visited.insert(params.unit_id);
888        queue.push_back((params.unit_id, 0u32, vec![params.unit_id]));
889
890        while let Some((current, depth, path)) = queue.pop_front() {
891            if current != params.unit_id {
892                nodes.push(DependencyNode {
893                    unit_id: current,
894                    depth,
895                    path: path.clone(),
896                });
897            }
898
899            if depth >= effective_depth {
900                continue;
901            }
902
903            // Follow INCOMING edges (reverse direction).
904            for edge in graph.edges_to(current) {
905                if !edge_types.contains(&edge.edge_type) {
906                    continue;
907                }
908                if visited.insert(edge.source_id) {
909                    let mut new_path = path.clone();
910                    new_path.push(edge.source_id);
911                    queue.push_back((edge.source_id, depth + 1, new_path));
912                }
913            }
914        }
915
916        Ok(DependencyResult {
917            root_id: params.unit_id,
918            nodes,
919        })
920    }
921
922    // ========================================================================
923    // Query 4: Call Graph
924    // ========================================================================
925
926    /// Build a call graph centered on a function.
927    pub fn call_graph(
928        &self,
929        graph: &CodeGraph,
930        params: CallGraphParams,
931    ) -> AcbResult<CallGraphResult> {
932        self.validate_unit(graph, params.unit_id)?;
933
934        let mut all_nodes: Vec<(u64, u32)> = Vec::new();
935        let mut call_sites = Vec::new();
936        let mut seen = HashSet::new();
937
938        // Callees: outgoing Calls edges.
939        if params.direction == CallDirection::Callees || params.direction == CallDirection::Both {
940            let opts = TraversalOptions {
941                max_depth: params.max_depth as i32,
942                edge_types: vec![EdgeType::Calls],
943                direction: Direction::Forward,
944            };
945            let results = traversal::bfs(graph, params.unit_id, &opts);
946            for (id, depth) in &results {
947                if seen.insert(*id) {
948                    all_nodes.push((*id, *depth));
949                }
950            }
951
952            // Collect call sites from forward edges.
953            self.collect_call_sites_forward(
954                graph,
955                params.unit_id,
956                params.max_depth,
957                &mut call_sites,
958            );
959        }
960
961        // Callers: incoming Calls edges.
962        if params.direction == CallDirection::Callers || params.direction == CallDirection::Both {
963            let opts = TraversalOptions {
964                max_depth: params.max_depth as i32,
965                edge_types: vec![EdgeType::Calls],
966                direction: Direction::Backward,
967            };
968            let results = traversal::bfs(graph, params.unit_id, &opts);
969            for (id, depth) in &results {
970                if seen.insert(*id) {
971                    all_nodes.push((*id, *depth));
972                }
973            }
974
975            // Collect call sites from backward edges.
976            self.collect_call_sites_backward(
977                graph,
978                params.unit_id,
979                params.max_depth,
980                &mut call_sites,
981            );
982        }
983
984        Ok(CallGraphResult {
985            root_id: params.unit_id,
986            nodes: all_nodes,
987            call_sites,
988        })
989    }
990
991    // ========================================================================
992    // Query 5: Type Hierarchy
993    // ========================================================================
994
995    /// Explore the type hierarchy (ancestors and/or descendants).
996    pub fn type_hierarchy(
997        &self,
998        graph: &CodeGraph,
999        params: HierarchyParams,
1000    ) -> AcbResult<HierarchyResult> {
1001        self.validate_unit(graph, params.unit_id)?;
1002
1003        let mut nodes = Vec::new();
1004
1005        if params.include_ancestors {
1006            // Follow outgoing Inherits/Implements edges upward.
1007            let mut visited = HashSet::new();
1008            let mut queue = VecDeque::new();
1009            visited.insert(params.unit_id);
1010            queue.push_back((params.unit_id, 0i32));
1011
1012            while let Some((current, depth)) = queue.pop_front() {
1013                for edge in graph.edges_from(current) {
1014                    if edge.edge_type != EdgeType::Inherits
1015                        && edge.edge_type != EdgeType::Implements
1016                    {
1017                        continue;
1018                    }
1019                    if visited.insert(edge.target_id) {
1020                        let ancestor_depth = depth + 1;
1021                        nodes.push(HierarchyNode {
1022                            unit_id: edge.target_id,
1023                            relation: edge.edge_type,
1024                            depth: ancestor_depth,
1025                        });
1026                        queue.push_back((edge.target_id, ancestor_depth));
1027                    }
1028                }
1029            }
1030        }
1031
1032        if params.include_descendants {
1033            // Follow incoming Inherits/Implements edges downward.
1034            let mut visited = HashSet::new();
1035            let mut queue = VecDeque::new();
1036            visited.insert(params.unit_id);
1037            queue.push_back((params.unit_id, 0i32));
1038
1039            while let Some((current, depth)) = queue.pop_front() {
1040                for edge in graph.edges_to(current) {
1041                    if edge.edge_type != EdgeType::Inherits
1042                        && edge.edge_type != EdgeType::Implements
1043                    {
1044                        continue;
1045                    }
1046                    if visited.insert(edge.source_id) {
1047                        let desc_depth = depth - 1;
1048                        nodes.push(HierarchyNode {
1049                            unit_id: edge.source_id,
1050                            relation: edge.edge_type,
1051                            depth: desc_depth,
1052                        });
1053                        queue.push_back((edge.source_id, desc_depth));
1054                    }
1055                }
1056            }
1057        }
1058
1059        Ok(HierarchyResult {
1060            root_id: params.unit_id,
1061            nodes,
1062        })
1063    }
1064
1065    // ========================================================================
1066    // Query 6: Containment
1067    // ========================================================================
1068
1069    /// Find all units contained within the given unit, recursively.
1070    pub fn containment<'g>(
1071        &self,
1072        graph: &'g CodeGraph,
1073        unit_id: u64,
1074    ) -> AcbResult<Vec<&'g CodeUnit>> {
1075        self.validate_unit(graph, unit_id)?;
1076
1077        let opts = TraversalOptions {
1078            max_depth: -1,
1079            edge_types: vec![EdgeType::Contains],
1080            direction: Direction::Forward,
1081        };
1082
1083        let traversal = traversal::bfs(graph, unit_id, &opts);
1084        let mut result = Vec::new();
1085        for (id, _depth) in traversal {
1086            if id == unit_id {
1087                continue; // skip the root
1088            }
1089            if let Some(unit) = graph.get_unit(id) {
1090                result.push(unit);
1091            }
1092        }
1093
1094        Ok(result)
1095    }
1096
1097    // ========================================================================
1098    // Query 7: Pattern Match
1099    // ========================================================================
1100
1101    /// Match units against a simple pattern DSL.
1102    pub fn pattern_match(
1103        &self,
1104        graph: &CodeGraph,
1105        params: PatternParams,
1106    ) -> AcbResult<Vec<PatternMatch>> {
1107        let pattern = params.pattern.trim();
1108        let mut results = Vec::new();
1109
1110        // Parse `async function` patterns.
1111        if pattern.starts_with("async function") || pattern.starts_with("async_function") {
1112            // Match complexity constraint if present.
1113            let complexity_threshold = self.parse_complexity_constraint(pattern);
1114
1115            for unit in graph.units() {
1116                if unit.unit_type == CodeUnitType::Function && unit.is_async {
1117                    if let Some(threshold) = complexity_threshold {
1118                        if unit.complexity <= threshold {
1119                            continue;
1120                        }
1121                    }
1122                    results.push(PatternMatch {
1123                        unit_id: unit.id,
1124                        confidence: 1.0,
1125                        matched_rule: "async function".to_string(),
1126                    });
1127                }
1128            }
1129            return Ok(results);
1130        }
1131
1132        // Parse `function { calls: [A, B] }`.
1133        if pattern.starts_with("function") && pattern.contains("calls:") {
1134            let call_targets = self.parse_call_list(pattern);
1135            let complexity_threshold = self.parse_complexity_constraint(pattern);
1136
1137            for unit in graph.units() {
1138                if unit.unit_type != CodeUnitType::Function {
1139                    continue;
1140                }
1141                if let Some(threshold) = complexity_threshold {
1142                    if unit.complexity <= threshold {
1143                        continue;
1144                    }
1145                }
1146                let callees: HashSet<String> = graph
1147                    .edges_from_of_type(unit.id, EdgeType::Calls)
1148                    .iter()
1149                    .filter_map(|e| graph.get_unit(e.target_id))
1150                    .map(|u| u.name.clone())
1151                    .collect();
1152
1153                if call_targets.iter().all(|t| callees.contains(t)) {
1154                    results.push(PatternMatch {
1155                        unit_id: unit.id,
1156                        confidence: 1.0,
1157                        matched_rule: format!("function calls {:?}", call_targets),
1158                    });
1159                }
1160            }
1161            return Ok(results);
1162        }
1163
1164        // Parse `class { inherits: Base }`.
1165        if pattern.starts_with("class") && pattern.contains("inherits:") {
1166            let base_name = self.parse_inherits_target(pattern);
1167            if let Some(base) = base_name {
1168                for unit in graph.units() {
1169                    if unit.unit_type != CodeUnitType::Type {
1170                        continue;
1171                    }
1172                    let parents: Vec<String> = graph
1173                        .edges_from_of_type(unit.id, EdgeType::Inherits)
1174                        .iter()
1175                        .filter_map(|e| graph.get_unit(e.target_id))
1176                        .map(|u| u.name.clone())
1177                        .collect();
1178                    if parents.contains(&base) {
1179                        results.push(PatternMatch {
1180                            unit_id: unit.id,
1181                            confidence: 1.0,
1182                            matched_rule: format!("class inherits {}", base),
1183                        });
1184                    }
1185                }
1186            }
1187            return Ok(results);
1188        }
1189
1190        // Parse `function { complexity: >N }`.
1191        if pattern.starts_with("function") && pattern.contains("complexity:") {
1192            let threshold = self.parse_complexity_constraint(pattern);
1193            if let Some(t) = threshold {
1194                for unit in graph.units() {
1195                    if unit.unit_type == CodeUnitType::Function && unit.complexity > t {
1196                        results.push(PatternMatch {
1197                            unit_id: unit.id,
1198                            confidence: 1.0,
1199                            matched_rule: format!("function complexity > {}", t),
1200                        });
1201                    }
1202                }
1203            }
1204            return Ok(results);
1205        }
1206
1207        // Fallback: no pattern matched.
1208        Err(AcbError::QueryError(format!(
1209            "Unrecognized pattern: {}",
1210            pattern
1211        )))
1212    }
1213
1214    // ========================================================================
1215    // Query 8: Semantic Search
1216    // ========================================================================
1217
1218    /// Brute-force cosine-similarity search over feature vectors.
1219    pub fn semantic_search(
1220        &self,
1221        graph: &CodeGraph,
1222        params: SemanticParams,
1223    ) -> AcbResult<Vec<SemanticMatch>> {
1224        if params.query_vec.is_empty() {
1225            return Err(AcbError::QueryError("Query vector is empty".to_string()));
1226        }
1227
1228        let mut scored: Vec<SemanticMatch> = graph
1229            .units()
1230            .iter()
1231            .filter(|u| params.unit_types.is_empty() || params.unit_types.contains(&u.unit_type))
1232            .filter(|u| params.languages.is_empty() || params.languages.contains(&u.language))
1233            .filter_map(|u| {
1234                let score = cosine_similarity(&params.query_vec, &u.feature_vec);
1235                if score >= params.min_similarity {
1236                    Some(SemanticMatch {
1237                        unit_id: u.id,
1238                        score,
1239                    })
1240                } else {
1241                    None
1242                }
1243            })
1244            .collect();
1245
1246        scored.sort_by(|a, b| {
1247            b.score
1248                .partial_cmp(&a.score)
1249                .unwrap_or(std::cmp::Ordering::Equal)
1250        });
1251
1252        if params.top_k > 0 {
1253            scored.truncate(params.top_k);
1254        }
1255
1256        Ok(scored)
1257    }
1258
1259    // ========================================================================
1260    // Query 9: Impact Analysis
1261    // ========================================================================
1262
1263    /// Analyse the impact of changing a unit.
1264    pub fn impact_analysis(
1265        &self,
1266        graph: &CodeGraph,
1267        params: ImpactParams,
1268    ) -> AcbResult<ImpactResult> {
1269        self.validate_unit(graph, params.unit_id)?;
1270
1271        // Use reverse_dependency to find who depends on this unit.
1272        let dep_params = DependencyParams {
1273            unit_id: params.unit_id,
1274            max_depth: params.max_depth,
1275            edge_types: params.edge_types.clone(),
1276            include_transitive: true,
1277        };
1278        let deps = self.reverse_dependency(graph, dep_params)?;
1279
1280        let mut impacted = Vec::new();
1281        let mut total_risk = 0.0_f32;
1282
1283        for node in &deps.nodes {
1284            let has_tests = !graph
1285                .edges_to_of_type(node.unit_id, EdgeType::Tests)
1286                .is_empty();
1287
1288            // Coupling count.
1289            let coupling_count = graph
1290                .edges_from_of_type(node.unit_id, EdgeType::CouplesWith)
1291                .len()
1292                + graph
1293                    .edges_to_of_type(node.unit_id, EdgeType::CouplesWith)
1294                    .len();
1295
1296            // Risk score: base from depth, increased if no tests or high coupling.
1297            let depth_factor = 1.0 / (1.0 + node.depth as f32);
1298            let test_factor = if has_tests { 0.2 } else { 0.6 };
1299            let coupling_factor = (coupling_count as f32 * 0.05).min(0.3);
1300            let risk = (depth_factor * 0.4 + test_factor + coupling_factor).min(1.0);
1301
1302            total_risk += risk;
1303
1304            impacted.push(ImpactedUnit {
1305                unit_id: node.unit_id,
1306                depth: node.depth,
1307                risk_score: risk,
1308                has_tests,
1309            });
1310        }
1311
1312        let overall_risk = if impacted.is_empty() {
1313            0.0
1314        } else {
1315            (total_risk / impacted.len() as f32).min(1.0)
1316        };
1317
1318        let mut recommendations = Vec::new();
1319        let untested_count = impacted.iter().filter(|i| !i.has_tests).count();
1320        if untested_count > 0 {
1321            recommendations.push(format!(
1322                "Add tests for {} impacted units that lack test coverage.",
1323                untested_count
1324            ));
1325        }
1326        if impacted.len() > 10 {
1327            recommendations.push(
1328                "Consider breaking this unit into smaller pieces to reduce blast radius."
1329                    .to_string(),
1330            );
1331        }
1332        if overall_risk > 0.7 {
1333            recommendations
1334                .push("High overall risk. Deploy incrementally with feature flags.".to_string());
1335        }
1336
1337        Ok(ImpactResult {
1338            root_id: params.unit_id,
1339            impacted,
1340            overall_risk,
1341            recommendations,
1342        })
1343    }
1344
1345    // ========================================================================
1346    // Query 10: Test Coverage
1347    // ========================================================================
1348
1349    /// Compute test coverage information for a unit.
1350    pub fn test_coverage(&self, graph: &CodeGraph, unit_id: u64) -> AcbResult<CoverageResult> {
1351        self.validate_unit(graph, unit_id)?;
1352
1353        // Direct tests: units that have a Tests edge pointing TO this unit.
1354        let direct_tests: Vec<u64> = graph
1355            .edges_to_of_type(unit_id, EdgeType::Tests)
1356            .iter()
1357            .map(|e| e.source_id)
1358            .collect();
1359
1360        // Indirect tests: find callers of this unit, then find tests of those callers.
1361        let callers: Vec<u64> = graph
1362            .edges_to_of_type(unit_id, EdgeType::Calls)
1363            .iter()
1364            .map(|e| e.source_id)
1365            .collect();
1366
1367        let mut indirect_set: HashSet<u64> = HashSet::new();
1368        for caller_id in &callers {
1369            for test_edge in graph.edges_to_of_type(*caller_id, EdgeType::Tests) {
1370                indirect_set.insert(test_edge.source_id);
1371            }
1372        }
1373        // Remove any that are already direct tests.
1374        for dt in &direct_tests {
1375            indirect_set.remove(dt);
1376        }
1377        let indirect_tests: Vec<u64> = indirect_set.into_iter().collect();
1378
1379        // Estimate coverage ratio.
1380        let total_tests = direct_tests.len() + indirect_tests.len();
1381        let coverage_ratio = if total_tests > 0 {
1382            // Heuristic: direct tests count fully, indirect count as half.
1383            let effective = direct_tests.len() as f32 + indirect_tests.len() as f32 * 0.5;
1384            (effective / (effective + 1.0)).min(1.0)
1385        } else {
1386            0.0
1387        };
1388
1389        Ok(CoverageResult {
1390            unit_id,
1391            direct_tests,
1392            indirect_tests,
1393            coverage_ratio,
1394        })
1395    }
1396
1397    // ========================================================================
1398    // Query 11: Cross-Language Trace
1399    // ========================================================================
1400
1401    /// Trace FFI bindings across language boundaries.
1402    pub fn cross_language_trace(
1403        &self,
1404        graph: &CodeGraph,
1405        params: TraceParams,
1406    ) -> AcbResult<TraceResult> {
1407        self.validate_unit(graph, params.unit_id)?;
1408
1409        let start_unit = graph
1410            .get_unit(params.unit_id)
1411            .ok_or(AcbError::UnitNotFound(params.unit_id))?;
1412
1413        let mut hops = Vec::new();
1414        let mut languages_crossed = Vec::new();
1415
1416        hops.push(TraceHop {
1417            unit_id: params.unit_id,
1418            language: start_unit.language,
1419            via_edge: None,
1420        });
1421        languages_crossed.push(start_unit.language);
1422
1423        let mut visited = HashSet::new();
1424        visited.insert(params.unit_id);
1425        let mut current_frontier = vec![params.unit_id];
1426
1427        for _hop in 0..params.max_hops {
1428            let mut next_frontier = Vec::new();
1429            for current_id in &current_frontier {
1430                // Follow FfiBinds edges in both directions.
1431                for edge in graph.edges_from(*current_id) {
1432                    if edge.edge_type != EdgeType::FfiBinds {
1433                        continue;
1434                    }
1435                    if visited.insert(edge.target_id) {
1436                        if let Some(target_unit) = graph.get_unit(edge.target_id) {
1437                            hops.push(TraceHop {
1438                                unit_id: edge.target_id,
1439                                language: target_unit.language,
1440                                via_edge: Some(EdgeType::FfiBinds),
1441                            });
1442                            if !languages_crossed.contains(&target_unit.language) {
1443                                languages_crossed.push(target_unit.language);
1444                            }
1445                            next_frontier.push(edge.target_id);
1446                        }
1447                    }
1448                }
1449                for edge in graph.edges_to(*current_id) {
1450                    if edge.edge_type != EdgeType::FfiBinds {
1451                        continue;
1452                    }
1453                    if visited.insert(edge.source_id) {
1454                        if let Some(source_unit) = graph.get_unit(edge.source_id) {
1455                            hops.push(TraceHop {
1456                                unit_id: edge.source_id,
1457                                language: source_unit.language,
1458                                via_edge: Some(EdgeType::FfiBinds),
1459                            });
1460                            if !languages_crossed.contains(&source_unit.language) {
1461                                languages_crossed.push(source_unit.language);
1462                            }
1463                            next_frontier.push(edge.source_id);
1464                        }
1465                    }
1466                }
1467            }
1468            if next_frontier.is_empty() {
1469                break;
1470            }
1471            current_frontier = next_frontier;
1472        }
1473
1474        Ok(TraceResult {
1475            hops,
1476            languages_crossed,
1477        })
1478    }
1479
1480    // ========================================================================
1481    // Query 12: Collective Patterns (placeholder)
1482    // ========================================================================
1483
1484    /// Query collective intelligence patterns.
1485    ///
1486    /// Currently returns a placeholder result since the collective
1487    /// intelligence backend is not yet integrated.
1488    pub fn collective_patterns(
1489        &self,
1490        graph: &CodeGraph,
1491        params: CollectiveParams,
1492    ) -> AcbResult<CollectiveResult> {
1493        // Filter units by collective usage.
1494        let mut patterns: Vec<CollectivePatternEntry> = graph
1495            .units()
1496            .iter()
1497            .filter(|u| {
1498                if let Some(ref ut) = params.unit_type {
1499                    u.unit_type == *ut
1500                } else {
1501                    true
1502                }
1503            })
1504            .filter(|u| u.collective_usage >= params.min_usage)
1505            .map(|u| CollectivePatternEntry {
1506                unit_id: u.id,
1507                usage_count: u.collective_usage,
1508                confidence: if u.collective_usage > 0 {
1509                    (u.collective_usage as f32).ln().min(1.0)
1510                } else {
1511                    0.0
1512                },
1513            })
1514            .collect();
1515
1516        patterns.sort_by(|a, b| b.usage_count.cmp(&a.usage_count));
1517
1518        if params.limit > 0 {
1519            patterns.truncate(params.limit);
1520        }
1521
1522        Ok(CollectiveResult {
1523            patterns,
1524            collective_available: false,
1525        })
1526    }
1527
1528    // ========================================================================
1529    // Query 13: Temporal Evolution
1530    // ========================================================================
1531
1532    /// Get the temporal evolution of a code unit.
1533    pub fn temporal_evolution(
1534        &self,
1535        graph: &CodeGraph,
1536        unit_id: u64,
1537    ) -> AcbResult<EvolutionResult> {
1538        let unit = graph
1539            .get_unit(unit_id)
1540            .ok_or(AcbError::UnitNotFound(unit_id))?;
1541
1542        let trend = if unit.stability_score > 0.8 {
1543            "Stable — rarely changes.".to_string()
1544        } else if unit.stability_score > 0.5 {
1545            "Moderate — changes occasionally.".to_string()
1546        } else if unit.stability_score > 0.2 {
1547            "Volatile — changes frequently.".to_string()
1548        } else {
1549            "Highly volatile — constantly changing, potential hotspot.".to_string()
1550        };
1551
1552        Ok(EvolutionResult {
1553            unit_id,
1554            change_count: unit.change_count,
1555            created_at: unit.created_at,
1556            last_modified: unit.last_modified,
1557            stability_score: unit.stability_score,
1558            trend,
1559        })
1560    }
1561
1562    // ========================================================================
1563    // Query 14: Stability Analysis
1564    // ========================================================================
1565
1566    /// Analyse the stability of a code unit.
1567    pub fn stability_analysis(
1568        &self,
1569        graph: &CodeGraph,
1570        unit_id: u64,
1571    ) -> AcbResult<StabilityResult> {
1572        let unit = graph
1573            .get_unit(unit_id)
1574            .ok_or(AcbError::UnitNotFound(unit_id))?;
1575
1576        let mut factors = Vec::new();
1577
1578        // Factor 1: Change frequency.
1579        let change_factor = 1.0 / (1.0 + unit.change_count as f32 * 0.1);
1580        factors.push(StabilityFactor {
1581            name: "change_frequency".to_string(),
1582            value: change_factor,
1583            description: format!(
1584                "Unit has {} changes. Lower change count = more stable.",
1585                unit.change_count
1586            ),
1587        });
1588
1589        // Factor 2: Test coverage.
1590        let test_count = graph.edges_to_of_type(unit_id, EdgeType::Tests).len();
1591        let test_factor = if test_count > 0 {
1592            (test_count as f32 * 0.3).min(1.0)
1593        } else {
1594            0.0
1595        };
1596        factors.push(StabilityFactor {
1597            name: "test_coverage".to_string(),
1598            value: test_factor,
1599            description: format!(
1600                "{} direct tests. More tests = more stability confidence.",
1601                test_count
1602            ),
1603        });
1604
1605        // Factor 3: Complexity.
1606        let complexity_factor = 1.0 / (1.0 + unit.complexity as f32 * 0.05);
1607        factors.push(StabilityFactor {
1608            name: "complexity".to_string(),
1609            value: complexity_factor,
1610            description: format!(
1611                "Cyclomatic complexity is {}. Lower = more stable.",
1612                unit.complexity
1613            ),
1614        });
1615
1616        // Factor 4: Coupling.
1617        let coupling_count = graph
1618            .edges_from_of_type(unit_id, EdgeType::CouplesWith)
1619            .len()
1620            + graph.edges_to_of_type(unit_id, EdgeType::CouplesWith).len();
1621        let coupling_factor = 1.0 / (1.0 + coupling_count as f32 * 0.2);
1622        factors.push(StabilityFactor {
1623            name: "coupling".to_string(),
1624            value: coupling_factor,
1625            description: format!(
1626                "{} temporal couplings. Fewer couplings = more independent.",
1627                coupling_count
1628            ),
1629        });
1630
1631        // Overall score: weighted average.
1632        let overall = change_factor * 0.3
1633            + test_factor * 0.25
1634            + complexity_factor * 0.25
1635            + coupling_factor * 0.2;
1636
1637        let recommendation = if overall > 0.7 {
1638            "Unit is stable. No immediate action needed.".to_string()
1639        } else if overall > 0.4 {
1640            "Unit has moderate stability. Consider adding tests and reducing complexity."
1641                .to_string()
1642        } else {
1643            "Unit is unstable. Prioritize adding tests, reducing complexity, and decoupling."
1644                .to_string()
1645        };
1646
1647        Ok(StabilityResult {
1648            unit_id,
1649            overall_score: overall,
1650            factors,
1651            recommendation,
1652        })
1653    }
1654
1655    // ========================================================================
1656    // Query 15: Coupling Detection
1657    // ========================================================================
1658
1659    /// Detect couplings between code units.
1660    pub fn coupling_detection(
1661        &self,
1662        graph: &CodeGraph,
1663        params: CouplingParams,
1664    ) -> AcbResult<Vec<Coupling>> {
1665        let mut couplings = Vec::new();
1666        let mut seen_pairs: HashSet<(u64, u64)> = HashSet::new();
1667
1668        let units_to_check: Vec<u64> = if let Some(uid) = params.unit_id {
1669            self.validate_unit(graph, uid)?;
1670            vec![uid]
1671        } else {
1672            graph.units().iter().map(|u| u.id).collect()
1673        };
1674
1675        for uid in &units_to_check {
1676            // Explicit couplings: direct dependency edges.
1677            for edge in graph.edges_from(*uid) {
1678                if !edge.edge_type.is_dependency() {
1679                    continue;
1680                }
1681                let pair = normalize_pair(*uid, edge.target_id);
1682                if edge.weight >= params.min_strength && seen_pairs.insert(pair) {
1683                    couplings.push(Coupling {
1684                        unit_a: pair.0,
1685                        unit_b: pair.1,
1686                        strength: edge.weight,
1687                        kind: CouplingKind::Explicit,
1688                    });
1689                }
1690            }
1691
1692            // Temporal couplings: CouplesWith edges.
1693            for edge in graph.edges_from_of_type(*uid, EdgeType::CouplesWith) {
1694                let pair = normalize_pair(*uid, edge.target_id);
1695                if edge.weight >= params.min_strength && seen_pairs.insert(pair) {
1696                    couplings.push(Coupling {
1697                        unit_a: pair.0,
1698                        unit_b: pair.1,
1699                        strength: edge.weight,
1700                        kind: CouplingKind::Temporal,
1701                    });
1702                }
1703            }
1704            for edge in graph.edges_to_of_type(*uid, EdgeType::CouplesWith) {
1705                let pair = normalize_pair(edge.source_id, *uid);
1706                if edge.weight >= params.min_strength && seen_pairs.insert(pair) {
1707                    couplings.push(Coupling {
1708                        unit_a: pair.0,
1709                        unit_b: pair.1,
1710                        strength: edge.weight,
1711                        kind: CouplingKind::Temporal,
1712                    });
1713                }
1714            }
1715        }
1716
1717        // Hidden couplings: units that share many outgoing targets.
1718        if let Some(uid) = params.unit_id {
1719            let my_targets: HashSet<u64> = graph
1720                .edges_from(uid)
1721                .iter()
1722                .filter(|e| e.edge_type.is_dependency())
1723                .map(|e| e.target_id)
1724                .collect();
1725
1726            if !my_targets.is_empty() {
1727                for other_unit in graph.units() {
1728                    if other_unit.id == uid {
1729                        continue;
1730                    }
1731                    let other_targets: HashSet<u64> = graph
1732                        .edges_from(other_unit.id)
1733                        .iter()
1734                        .filter(|e| e.edge_type.is_dependency())
1735                        .map(|e| e.target_id)
1736                        .collect();
1737
1738                    if other_targets.is_empty() {
1739                        continue;
1740                    }
1741
1742                    let intersection = my_targets.intersection(&other_targets).count();
1743                    let union = my_targets.union(&other_targets).count();
1744                    let jaccard = if union > 0 {
1745                        intersection as f32 / union as f32
1746                    } else {
1747                        0.0
1748                    };
1749
1750                    if jaccard >= params.min_strength {
1751                        let pair = normalize_pair(uid, other_unit.id);
1752                        if seen_pairs.insert(pair) {
1753                            couplings.push(Coupling {
1754                                unit_a: pair.0,
1755                                unit_b: pair.1,
1756                                strength: jaccard,
1757                                kind: CouplingKind::Hidden,
1758                            });
1759                        }
1760                    }
1761                }
1762            }
1763        }
1764
1765        // Sort by strength descending.
1766        couplings.sort_by(|a, b| {
1767            b.strength
1768                .partial_cmp(&a.strength)
1769                .unwrap_or(std::cmp::Ordering::Equal)
1770        });
1771
1772        Ok(couplings)
1773    }
1774
1775    // ========================================================================
1776    // Query 16: Dead Code
1777    // ========================================================================
1778
1779    /// Detect potentially dead (unreachable) code.
1780    pub fn dead_code<'g>(
1781        &self,
1782        graph: &'g CodeGraph,
1783        params: DeadCodeParams,
1784    ) -> AcbResult<Vec<&'g CodeUnit>> {
1785        // Identify entry points: main functions, tests, public module exports.
1786        let mut roots: HashSet<u64> = HashSet::new();
1787
1788        for unit in graph.units() {
1789            let is_entry = match unit.unit_type {
1790                CodeUnitType::Function => {
1791                    unit.name == "main"
1792                        || unit.name.starts_with("test_")
1793                        || unit.visibility == Visibility::Public
1794                }
1795                CodeUnitType::Test => true,
1796                CodeUnitType::Module => {
1797                    // Modules are implicit roots.
1798                    unit.visibility == Visibility::Public
1799                }
1800                _ => unit.visibility == Visibility::Public,
1801            };
1802            if is_entry {
1803                roots.insert(unit.id);
1804            }
1805        }
1806
1807        if params.include_tests_as_roots {
1808            for unit in graph.find_units_by_type(CodeUnitType::Test) {
1809                roots.insert(unit.id);
1810            }
1811        }
1812
1813        // Run reachability from all roots.
1814        let mut reachable: HashSet<u64> = HashSet::new();
1815        let opts = TraversalOptions {
1816            max_depth: -1,
1817            edge_types: Vec::new(), // all edge types
1818            direction: Direction::Forward,
1819        };
1820
1821        for root_id in &roots {
1822            let visited = traversal::bfs(graph, *root_id, &opts);
1823            for (id, _) in visited {
1824                reachable.insert(id);
1825            }
1826        }
1827
1828        // Also run backward reachability from roots to catch units
1829        // that are reached via incoming edges (e.g., tests targeting roots).
1830        let back_opts = TraversalOptions {
1831            max_depth: -1,
1832            edge_types: Vec::new(),
1833            direction: Direction::Backward,
1834        };
1835        for root_id in &roots {
1836            let visited = traversal::bfs(graph, *root_id, &back_opts);
1837            for (id, _) in visited {
1838                reachable.insert(id);
1839            }
1840        }
1841
1842        // Find unreachable units.
1843        let mut dead: Vec<&CodeUnit> = graph
1844            .units()
1845            .iter()
1846            .filter(|u| {
1847                if params.unit_types.is_empty() {
1848                    true
1849                } else {
1850                    params.unit_types.contains(&u.unit_type)
1851                }
1852            })
1853            .filter(|u| !reachable.contains(&u.id))
1854            .collect();
1855
1856        // Sort by name for deterministic output.
1857        dead.sort_by(|a, b| a.name.cmp(&b.name));
1858
1859        Ok(dead)
1860    }
1861
1862    // ========================================================================
1863    // Query 17: Prophecy
1864    // ========================================================================
1865
1866    /// Predict likely future breakages based on code patterns.
1867    pub fn prophecy(&self, graph: &CodeGraph, params: ProphecyParams) -> AcbResult<ProphecyResult> {
1868        let mut predictions = Vec::new();
1869
1870        for unit in graph.units() {
1871            let mut risk = 0.0_f32;
1872            let mut reasons = Vec::new();
1873
1874            // Low stability = higher risk.
1875            if unit.stability_score < 0.3 {
1876                risk += 0.4;
1877                reasons.push(format!("Low stability score ({:.2})", unit.stability_score));
1878            } else if unit.stability_score < 0.6 {
1879                risk += 0.2;
1880                reasons.push(format!(
1881                    "Moderate stability score ({:.2})",
1882                    unit.stability_score
1883                ));
1884            }
1885
1886            // High complexity = higher risk.
1887            if unit.complexity > 20 {
1888                risk += 0.3;
1889                reasons.push(format!("High complexity ({})", unit.complexity));
1890            } else if unit.complexity > 10 {
1891                risk += 0.15;
1892                reasons.push(format!("Moderate complexity ({})", unit.complexity));
1893            }
1894
1895            // High change count = higher risk.
1896            if unit.change_count > 50 {
1897                risk += 0.2;
1898                reasons.push(format!("Frequently changed ({} times)", unit.change_count));
1899            } else if unit.change_count > 20 {
1900                risk += 0.1;
1901                reasons.push(format!("Changed {} times", unit.change_count));
1902            }
1903
1904            // BreaksWith edges = historical breakage indicator.
1905            let breaks_count = graph
1906                .edges_from_of_type(unit.id, EdgeType::BreaksWith)
1907                .len()
1908                + graph.edges_to_of_type(unit.id, EdgeType::BreaksWith).len();
1909            if breaks_count > 0 {
1910                risk += 0.2 * (breaks_count as f32).min(3.0) / 3.0;
1911                reasons.push(format!(
1912                    "{} historical breakage relationships",
1913                    breaks_count
1914                ));
1915            }
1916
1917            // No test coverage = higher risk.
1918            let test_count = graph.edges_to_of_type(unit.id, EdgeType::Tests).len();
1919            if test_count == 0 && unit.unit_type == CodeUnitType::Function {
1920                risk += 0.1;
1921                reasons.push("No test coverage".to_string());
1922            }
1923
1924            risk = risk.min(1.0);
1925
1926            if risk >= params.min_risk {
1927                predictions.push(Prediction {
1928                    unit_id: unit.id,
1929                    risk_score: risk,
1930                    reason: reasons.join("; "),
1931                });
1932            }
1933        }
1934
1935        // Sort by risk descending.
1936        predictions.sort_by(|a, b| {
1937            b.risk_score
1938                .partial_cmp(&a.risk_score)
1939                .unwrap_or(std::cmp::Ordering::Equal)
1940        });
1941
1942        if params.top_k > 0 {
1943            predictions.truncate(params.top_k);
1944        }
1945
1946        Ok(ProphecyResult { predictions })
1947    }
1948
1949    // ========================================================================
1950    // Query 18: Concept Mapping
1951    // ========================================================================
1952
1953    /// Map a concept to related code units.
1954    pub fn concept_mapping(&self, graph: &CodeGraph, concept: &str) -> AcbResult<ConceptMap> {
1955        let concept_lower = concept.to_lowercase();
1956        let mut units = Vec::new();
1957
1958        for unit in graph.units() {
1959            let name_match = unit.name.to_lowercase().contains(&concept_lower)
1960                || unit.qualified_name.to_lowercase().contains(&concept_lower);
1961            let doc_match = unit
1962                .doc_summary
1963                .as_ref()
1964                .map(|d| d.to_lowercase().contains(&concept_lower))
1965                .unwrap_or(false);
1966
1967            if !name_match && !doc_match {
1968                continue;
1969            }
1970
1971            // Determine the role.
1972            let role = match unit.unit_type {
1973                CodeUnitType::Type | CodeUnitType::Trait | CodeUnitType::Module => {
1974                    ConceptRole::Definition
1975                }
1976                CodeUnitType::Test => ConceptRole::Test,
1977                CodeUnitType::Impl => ConceptRole::Extension,
1978                _ => {
1979                    // Check if this unit extends something related to the concept.
1980                    let has_inherit = graph
1981                        .edges_from_of_type(unit.id, EdgeType::Inherits)
1982                        .iter()
1983                        .any(|e| {
1984                            graph
1985                                .get_unit(e.target_id)
1986                                .map(|t| t.name.to_lowercase().contains(&concept_lower))
1987                                .unwrap_or(false)
1988                        });
1989                    if has_inherit {
1990                        ConceptRole::Extension
1991                    } else {
1992                        ConceptRole::Usage
1993                    }
1994                }
1995            };
1996
1997            // Compute relevance.
1998            let mut relevance = 0.0_f32;
1999            if unit.name.to_lowercase() == concept_lower {
2000                relevance = 1.0;
2001            } else if name_match {
2002                relevance = 0.7;
2003            }
2004            if doc_match {
2005                relevance = (relevance + 0.3).min(1.0);
2006            }
2007
2008            units.push(ConceptUnit {
2009                unit_id: unit.id,
2010                role,
2011                relevance,
2012            });
2013        }
2014
2015        // Sort by relevance descending.
2016        units.sort_by(|a, b| {
2017            b.relevance
2018                .partial_cmp(&a.relevance)
2019                .unwrap_or(std::cmp::Ordering::Equal)
2020        });
2021
2022        Ok(ConceptMap {
2023            concept: concept.to_string(),
2024            units,
2025        })
2026    }
2027
2028    // ========================================================================
2029    // Query 19: Migration Path
2030    // ========================================================================
2031
2032    /// Plan a migration from one unit to another.
2033    pub fn migration_path(
2034        &self,
2035        graph: &CodeGraph,
2036        params: MigrationParams,
2037    ) -> AcbResult<MigrationPlan> {
2038        self.validate_unit(graph, params.from_unit)?;
2039        self.validate_unit(graph, params.to_unit)?;
2040
2041        // Find all dependents of from_unit (reverse deps).
2042        let dep_params = DependencyParams {
2043            unit_id: params.from_unit,
2044            max_depth: 10,
2045            edge_types: vec![
2046                EdgeType::Calls,
2047                EdgeType::Imports,
2048                EdgeType::UsesType,
2049                EdgeType::Inherits,
2050                EdgeType::Implements,
2051            ],
2052            include_transitive: false,
2053        };
2054        let deps = self.reverse_dependency(graph, dep_params)?;
2055
2056        let mut steps: Vec<MigrationStep> = Vec::new();
2057        let mut order = 0u32;
2058
2059        // Step 0: The target unit itself (create/prepare).
2060        steps.push(MigrationStep {
2061            unit_id: params.to_unit,
2062            order,
2063            safety: SafetyLevel::Safe,
2064            description: "Prepare the target unit as the replacement.".to_string(),
2065        });
2066        order += 1;
2067
2068        // Sort dependents by safety: tested first, then untested.
2069        let mut dependent_steps: Vec<(u64, SafetyLevel)> = Vec::new();
2070
2071        for node in &deps.nodes {
2072            let has_direct_tests = !graph
2073                .edges_to_of_type(node.unit_id, EdgeType::Tests)
2074                .is_empty();
2075            let callers = graph.edges_to_of_type(node.unit_id, EdgeType::Calls);
2076            let caller_tested = callers.iter().any(|e| {
2077                !graph
2078                    .edges_to_of_type(e.source_id, EdgeType::Tests)
2079                    .is_empty()
2080            });
2081
2082            let safety = if has_direct_tests {
2083                SafetyLevel::Safe
2084            } else if caller_tested {
2085                SafetyLevel::Caution
2086            } else {
2087                SafetyLevel::Risky
2088            };
2089
2090            dependent_steps.push((node.unit_id, safety));
2091        }
2092
2093        // Sort: Safe first, then Caution, then Risky.
2094        dependent_steps.sort_by(|a, b| a.1.cmp(&b.1));
2095
2096        for (uid, safety) in dependent_steps {
2097            let unit_name = graph
2098                .get_unit(uid)
2099                .map(|u| u.qualified_name.clone())
2100                .unwrap_or_else(|| format!("unit_{}", uid));
2101            let desc = match safety {
2102                SafetyLevel::Safe => {
2103                    format!("Update {} (has tests, safe to migrate).", unit_name)
2104                }
2105                SafetyLevel::Caution => {
2106                    format!(
2107                        "Update {} (no direct tests, but callers are tested — exercise caution).",
2108                        unit_name
2109                    )
2110                }
2111                SafetyLevel::Risky => {
2112                    format!(
2113                        "Update {} (no test coverage — add tests before migrating).",
2114                        unit_name
2115                    )
2116                }
2117            };
2118
2119            steps.push(MigrationStep {
2120                unit_id: uid,
2121                order,
2122                safety,
2123                description: desc,
2124            });
2125            order += 1;
2126        }
2127
2128        // Final step: remove the old unit.
2129        steps.push(MigrationStep {
2130            unit_id: params.from_unit,
2131            order,
2132            safety: SafetyLevel::Caution,
2133            description: "Remove or deprecate the original unit.".to_string(),
2134        });
2135
2136        Ok(MigrationPlan {
2137            from_unit: params.from_unit,
2138            to_unit: params.to_unit,
2139            steps,
2140        })
2141    }
2142
2143    // ========================================================================
2144    // Query 20: Test Gap
2145    // ========================================================================
2146
2147    /// Find units that have high change counts or complexity but no tests.
2148    pub fn test_gap(&self, graph: &CodeGraph, params: TestGapParams) -> AcbResult<Vec<TestGap>> {
2149        let target_types = if params.unit_types.is_empty() {
2150            vec![CodeUnitType::Function]
2151        } else {
2152            params.unit_types
2153        };
2154
2155        let mut gaps = Vec::new();
2156
2157        for unit in graph.units() {
2158            if !target_types.contains(&unit.unit_type) {
2159                continue;
2160            }
2161
2162            let has_tests = !graph.edges_to_of_type(unit.id, EdgeType::Tests).is_empty();
2163            if has_tests {
2164                continue;
2165            }
2166
2167            let high_changes = unit.change_count >= params.min_changes;
2168            let high_complexity = unit.complexity >= params.min_complexity;
2169
2170            if !high_changes && !high_complexity {
2171                continue;
2172            }
2173
2174            let mut reasons = Vec::new();
2175            if high_changes {
2176                reasons.push(format!("changed {} times", unit.change_count));
2177            }
2178            if high_complexity {
2179                reasons.push(format!("complexity {}", unit.complexity));
2180            }
2181
2182            // Priority: higher change count and higher complexity = more urgent.
2183            let change_score =
2184                (unit.change_count as f32 / params.min_changes.max(1) as f32).min(2.0);
2185            let complexity_score =
2186                (unit.complexity as f32 / params.min_complexity.max(1) as f32).min(2.0);
2187            let priority = (change_score * 0.5 + complexity_score * 0.5).min(1.0);
2188
2189            gaps.push(TestGap {
2190                unit_id: unit.id,
2191                reason: format!("No tests, but {}", reasons.join(" and ")),
2192                priority,
2193            });
2194        }
2195
2196        // Sort by priority descending.
2197        gaps.sort_by(|a, b| {
2198            b.priority
2199                .partial_cmp(&a.priority)
2200                .unwrap_or(std::cmp::Ordering::Equal)
2201        });
2202
2203        Ok(gaps)
2204    }
2205
2206    // ========================================================================
2207    // Query 21: Architectural Drift
2208    // ========================================================================
2209
2210    /// Check for architectural drift against declared rules.
2211    pub fn architectural_drift(
2212        &self,
2213        graph: &CodeGraph,
2214        params: DriftParams,
2215    ) -> AcbResult<DriftReport> {
2216        let mut violations = Vec::new();
2217
2218        for (rule_idx, rule) in params.rules.iter().enumerate() {
2219            match rule {
2220                ArchRule::LayerDependency { upper, lower } => {
2221                    // Lower layer must not depend on upper layer.
2222                    let lower_units: Vec<&CodeUnit> = graph
2223                        .units()
2224                        .iter()
2225                        .filter(|u| u.qualified_name.starts_with(lower.as_str()))
2226                        .collect();
2227
2228                    for lu in &lower_units {
2229                        for edge in graph.edges_from(lu.id) {
2230                            if !edge.edge_type.is_dependency() {
2231                                continue;
2232                            }
2233                            if let Some(target) = graph.get_unit(edge.target_id) {
2234                                if target.qualified_name.starts_with(upper.as_str()) {
2235                                    violations.push(DriftViolation {
2236                                        rule_index: rule_idx,
2237                                        description: format!(
2238                                            "Lower layer '{}' depends on upper layer '{}': {} -> {}",
2239                                            lower, upper, lu.qualified_name, target.qualified_name
2240                                        ),
2241                                        units: vec![lu.id, target.id],
2242                                    });
2243                                }
2244                            }
2245                        }
2246                    }
2247                }
2248                ArchRule::ModuleBoundary { module } => {
2249                    // Units in the module must not have dependencies outside.
2250                    let module_units: Vec<&CodeUnit> = graph
2251                        .units()
2252                        .iter()
2253                        .filter(|u| u.qualified_name.starts_with(module.as_str()))
2254                        .collect();
2255
2256                    for mu in &module_units {
2257                        for edge in graph.edges_from(mu.id) {
2258                            if !edge.edge_type.is_dependency() {
2259                                continue;
2260                            }
2261                            if let Some(target) = graph.get_unit(edge.target_id) {
2262                                if !target.qualified_name.starts_with(module.as_str()) {
2263                                    violations.push(DriftViolation {
2264                                        rule_index: rule_idx,
2265                                        description: format!(
2266                                            "Module boundary violation: {} depends on external {}",
2267                                            mu.qualified_name, target.qualified_name
2268                                        ),
2269                                        units: vec![mu.id, target.id],
2270                                    });
2271                                }
2272                            }
2273                        }
2274                    }
2275                }
2276                ArchRule::NamingConvention { prefix, pattern } => {
2277                    // Simple naming convention check.
2278                    // The pattern is treated as a required prefix or substring
2279                    // depending on its form:
2280                    //   - "test_*" means must start with "test_"
2281                    //   - "*_impl" means must end with "_impl"
2282                    //   - "*foo*" means must contain "foo"
2283                    //   - "exact" means must equal "exact"
2284                    let (check_start, check_end, _check_contains, literal) =
2285                        parse_simple_glob(pattern);
2286
2287                    for unit in graph.units() {
2288                        if !unit.qualified_name.starts_with(prefix.as_str()) {
2289                            continue;
2290                        }
2291
2292                        let name_lower = unit.name.to_lowercase();
2293                        let lit_lower = literal.to_lowercase();
2294
2295                        let name_matches = if check_start && check_end {
2296                            // *foo* — contains
2297                            name_lower.contains(&lit_lower)
2298                        } else if check_start {
2299                            // *foo — ends with
2300                            name_lower.ends_with(&lit_lower)
2301                        } else if check_end {
2302                            // foo* — starts with
2303                            name_lower.starts_with(&lit_lower)
2304                        } else {
2305                            // exact match
2306                            name_lower == lit_lower
2307                        };
2308
2309                        if !name_matches {
2310                            violations.push(DriftViolation {
2311                                rule_index: rule_idx,
2312                                description: format!(
2313                                    "Naming convention violation: '{}' does not match pattern '{}'",
2314                                    unit.name, pattern
2315                                ),
2316                                units: vec![unit.id],
2317                            });
2318                        }
2319                    }
2320                }
2321                ArchRule::Cyclic { scope } => {
2322                    // Check for cycles within the scoped units.
2323                    let scoped_ids: HashSet<u64> = graph
2324                        .units()
2325                        .iter()
2326                        .filter(|u| u.qualified_name.starts_with(scope.as_str()))
2327                        .map(|u| u.id)
2328                        .collect();
2329
2330                    if let Some(cycle) = self.detect_cycle(graph, &scoped_ids) {
2331                        let description = format!(
2332                            "Dependency cycle detected in scope '{}': {}",
2333                            scope,
2334                            cycle
2335                                .iter()
2336                                .filter_map(|id| graph.get_unit(*id).map(|u| u.name.clone()))
2337                                .collect::<Vec<_>>()
2338                                .join(" -> ")
2339                        );
2340                        violations.push(DriftViolation {
2341                            rule_index: rule_idx,
2342                            description,
2343                            units: cycle,
2344                        });
2345                    }
2346                }
2347            }
2348        }
2349
2350        let total_rules = params.rules.len();
2351        let violated_rules: HashSet<usize> = violations.iter().map(|v| v.rule_index).collect();
2352        let conformance_score = if total_rules > 0 {
2353            1.0 - (violated_rules.len() as f32 / total_rules as f32)
2354        } else {
2355            1.0
2356        };
2357
2358        Ok(DriftReport {
2359            violations,
2360            conformance_score,
2361        })
2362    }
2363
2364    // ========================================================================
2365    // Query 22: Similarity
2366    // ========================================================================
2367
2368    /// Find units similar to a given unit by feature vector.
2369    pub fn similarity(
2370        &self,
2371        graph: &CodeGraph,
2372        params: SimilarityParams,
2373    ) -> AcbResult<Vec<SimilarityMatch>> {
2374        let target = graph
2375            .get_unit(params.unit_id)
2376            .ok_or(AcbError::UnitNotFound(params.unit_id))?;
2377
2378        let target_vec = target.feature_vec.clone();
2379
2380        let mut matches: Vec<SimilarityMatch> = graph
2381            .units()
2382            .iter()
2383            .filter(|u| u.id != params.unit_id)
2384            .filter_map(|u| {
2385                let score = cosine_similarity(&target_vec, &u.feature_vec);
2386                if score >= params.min_similarity {
2387                    Some(SimilarityMatch {
2388                        unit_id: u.id,
2389                        score,
2390                    })
2391                } else {
2392                    None
2393                }
2394            })
2395            .collect();
2396
2397        matches.sort_by(|a, b| {
2398            b.score
2399                .partial_cmp(&a.score)
2400                .unwrap_or(std::cmp::Ordering::Equal)
2401        });
2402
2403        if params.top_k > 0 {
2404            matches.truncate(params.top_k);
2405        }
2406
2407        Ok(matches)
2408    }
2409
2410    // ========================================================================
2411    // Query 23: Shortest Path
2412    // ========================================================================
2413
2414    /// Find the shortest path between two units.
2415    pub fn shortest_path(&self, graph: &CodeGraph, from: u64, to: u64) -> AcbResult<PathResult> {
2416        self.validate_unit(graph, from)?;
2417        self.validate_unit(graph, to)?;
2418
2419        match traversal::shortest_path(graph, from, to, &[]) {
2420            Some(path) => {
2421                // Reconstruct edge types along the path.
2422                let mut edge_types = Vec::new();
2423                for window in path.windows(2) {
2424                    let src = window[0];
2425                    let tgt = window[1];
2426                    let et = graph
2427                        .edges_from(src)
2428                        .iter()
2429                        .find(|e| e.target_id == tgt)
2430                        .map(|e| e.edge_type)
2431                        .unwrap_or(EdgeType::References);
2432                    edge_types.push(et);
2433                }
2434                let length = path.len().saturating_sub(1);
2435                Ok(PathResult {
2436                    found: true,
2437                    path,
2438                    edge_types,
2439                    length,
2440                })
2441            }
2442            None => Ok(PathResult {
2443                found: false,
2444                path: Vec::new(),
2445                edge_types: Vec::new(),
2446                length: 0,
2447            }),
2448        }
2449    }
2450
2451    // ========================================================================
2452    // Query 24: Hotspot Detection
2453    // ========================================================================
2454
2455    /// Detect code hotspots based on change frequency, stability, and complexity.
2456    pub fn hotspot_detection(
2457        &self,
2458        graph: &CodeGraph,
2459        params: HotspotParams,
2460    ) -> AcbResult<Vec<Hotspot>> {
2461        let mut hotspots = Vec::new();
2462
2463        for unit in graph.units() {
2464            if !params.unit_types.is_empty() && !params.unit_types.contains(&unit.unit_type) {
2465                continue;
2466            }
2467
2468            let mut factors: HashMap<String, f32> = HashMap::new();
2469
2470            // Change frequency factor.
2471            let change_factor = (unit.change_count as f32 / 50.0).min(1.0);
2472            factors.insert("change_frequency".to_string(), change_factor);
2473
2474            // Instability factor (inverted stability).
2475            let instability = 1.0 - unit.stability_score;
2476            factors.insert("instability".to_string(), instability);
2477
2478            // Complexity factor.
2479            let complexity_factor = (unit.complexity as f32 / 30.0).min(1.0);
2480            factors.insert("complexity".to_string(), complexity_factor);
2481
2482            // Coupling factor.
2483            let coupling_count = graph.edges_from(unit.id).len() + graph.edges_to(unit.id).len();
2484            let coupling_factor = (coupling_count as f32 / 20.0).min(1.0);
2485            factors.insert("coupling".to_string(), coupling_factor);
2486
2487            // Weighted score.
2488            let score = change_factor * 0.35
2489                + instability * 0.25
2490                + complexity_factor * 0.25
2491                + coupling_factor * 0.15;
2492
2493            if score >= params.min_score {
2494                hotspots.push(Hotspot {
2495                    unit_id: unit.id,
2496                    score,
2497                    factors,
2498                });
2499            }
2500        }
2501
2502        // Sort by score descending.
2503        hotspots.sort_by(|a, b| {
2504            b.score
2505                .partial_cmp(&a.score)
2506                .unwrap_or(std::cmp::Ordering::Equal)
2507        });
2508
2509        if params.top_k > 0 {
2510            hotspots.truncate(params.top_k);
2511        }
2512
2513        Ok(hotspots)
2514    }
2515
2516    // ========================================================================
2517    // Private helpers
2518    // ========================================================================
2519
2520    /// Validate that a unit ID exists in the graph.
2521    fn validate_unit(&self, graph: &CodeGraph, unit_id: u64) -> AcbResult<()> {
2522        if graph.get_unit(unit_id).is_none() {
2523            return Err(AcbError::UnitNotFound(unit_id));
2524        }
2525        Ok(())
2526    }
2527
2528    /// Collect call sites following forward Calls edges (BFS).
2529    fn collect_call_sites_forward(
2530        &self,
2531        graph: &CodeGraph,
2532        start_id: u64,
2533        max_depth: u32,
2534        call_sites: &mut Vec<CallSite>,
2535    ) {
2536        let mut visited = HashSet::new();
2537        let mut queue = VecDeque::new();
2538        visited.insert(start_id);
2539        queue.push_back((start_id, 0u32));
2540
2541        while let Some((current, depth)) = queue.pop_front() {
2542            if depth >= max_depth {
2543                continue;
2544            }
2545            for edge in graph.edges_from_of_type(current, EdgeType::Calls) {
2546                call_sites.push(CallSite {
2547                    caller_id: current,
2548                    callee_id: edge.target_id,
2549                    span: Span::point(edge.context, 0),
2550                });
2551                if visited.insert(edge.target_id) {
2552                    queue.push_back((edge.target_id, depth + 1));
2553                }
2554            }
2555        }
2556    }
2557
2558    /// Collect call sites following backward Calls edges (BFS).
2559    fn collect_call_sites_backward(
2560        &self,
2561        graph: &CodeGraph,
2562        start_id: u64,
2563        max_depth: u32,
2564        call_sites: &mut Vec<CallSite>,
2565    ) {
2566        let mut visited = HashSet::new();
2567        let mut queue = VecDeque::new();
2568        visited.insert(start_id);
2569        queue.push_back((start_id, 0u32));
2570
2571        while let Some((current, depth)) = queue.pop_front() {
2572            if depth >= max_depth {
2573                continue;
2574            }
2575            for edge in graph.edges_to_of_type(current, EdgeType::Calls) {
2576                call_sites.push(CallSite {
2577                    caller_id: edge.source_id,
2578                    callee_id: current,
2579                    span: Span::point(edge.context, 0),
2580                });
2581                if visited.insert(edge.source_id) {
2582                    queue.push_back((edge.source_id, depth + 1));
2583                }
2584            }
2585        }
2586    }
2587
2588    /// Parse the `calls: [A, B, C]` list from a pattern string.
2589    fn parse_call_list(&self, pattern: &str) -> Vec<String> {
2590        if let Some(start) = pattern.find('[') {
2591            if let Some(end) = pattern.find(']') {
2592                if start < end {
2593                    let inner = &pattern[start + 1..end];
2594                    return inner
2595                        .split(',')
2596                        .map(|s| s.trim().to_string())
2597                        .filter(|s| !s.is_empty())
2598                        .collect();
2599                }
2600            }
2601        }
2602        Vec::new()
2603    }
2604
2605    /// Parse the `inherits: Base` target name from a pattern string.
2606    fn parse_inherits_target(&self, pattern: &str) -> Option<String> {
2607        if let Some(pos) = pattern.find("inherits:") {
2608            let after = &pattern[pos + "inherits:".len()..];
2609            let trimmed = after.trim().trim_end_matches('}').trim();
2610            let name = trimmed.split_whitespace().next()?;
2611            if !name.is_empty() {
2612                return Some(name.to_string());
2613            }
2614        }
2615        None
2616    }
2617
2618    /// Parse the `complexity: >N` constraint from a pattern string.
2619    fn parse_complexity_constraint(&self, pattern: &str) -> Option<u32> {
2620        if let Some(pos) = pattern.find("complexity:") {
2621            let after = &pattern[pos + "complexity:".len()..];
2622            let trimmed = after.trim().trim_start_matches('>').trim();
2623            let num_str: String = trimmed.chars().take_while(|c| c.is_ascii_digit()).collect();
2624            return num_str.parse::<u32>().ok();
2625        }
2626        None
2627    }
2628
2629    /// Detect a cycle in a subset of the graph using DFS-based cycle detection.
2630    ///
2631    /// Returns the first cycle found as a vector of unit IDs, or `None`.
2632    fn detect_cycle(&self, graph: &CodeGraph, scope: &HashSet<u64>) -> Option<Vec<u64>> {
2633        let mut visited = HashSet::new();
2634        let mut in_stack = HashSet::new();
2635        let mut stack = Vec::new();
2636
2637        for &uid in scope {
2638            if !visited.contains(&uid) {
2639                if let Some(cycle) = self.detect_cycle_dfs(
2640                    graph,
2641                    uid,
2642                    scope,
2643                    &mut visited,
2644                    &mut in_stack,
2645                    &mut stack,
2646                ) {
2647                    return Some(cycle);
2648                }
2649            }
2650        }
2651        None
2652    }
2653
2654    #[allow(clippy::only_used_in_recursion)]
2655    fn detect_cycle_dfs(
2656        &self,
2657        graph: &CodeGraph,
2658        uid: u64,
2659        scope: &HashSet<u64>,
2660        visited: &mut HashSet<u64>,
2661        in_stack: &mut HashSet<u64>,
2662        stack: &mut Vec<u64>,
2663    ) -> Option<Vec<u64>> {
2664        visited.insert(uid);
2665        in_stack.insert(uid);
2666        stack.push(uid);
2667
2668        for edge in graph.edges_from(uid) {
2669            if !edge.edge_type.is_dependency() {
2670                continue;
2671            }
2672            let target = edge.target_id;
2673            if !scope.contains(&target) {
2674                continue;
2675            }
2676
2677            if !visited.contains(&target) {
2678                if let Some(cycle) =
2679                    self.detect_cycle_dfs(graph, target, scope, visited, in_stack, stack)
2680                {
2681                    return Some(cycle);
2682                }
2683            } else if in_stack.contains(&target) {
2684                // Found a cycle — extract it from the stack.
2685                let pos = stack.iter().position(|&x| x == target)?;
2686                let mut cycle: Vec<u64> = stack[pos..].to_vec();
2687                cycle.push(target); // close the loop
2688                return Some(cycle);
2689            }
2690        }
2691
2692        stack.pop();
2693        in_stack.remove(&uid);
2694        None
2695    }
2696}
2697
2698impl Default for QueryEngine {
2699    fn default() -> Self {
2700        Self::new()
2701    }
2702}
2703
2704/// Parse a simple glob pattern into (starts_with_star, ends_with_star, is_contains, literal).
2705///
2706/// Examples:
2707/// - `"test_*"` -> `(false, true, false, "test_")`
2708/// - `"*_impl"` -> `(true, false, false, "_impl")`
2709/// - `"*foo*"` -> `(true, true, true, "foo")`
2710/// - `"exact"` -> `(false, false, false, "exact")`
2711fn parse_simple_glob(pattern: &str) -> (bool, bool, bool, String) {
2712    let starts = pattern.starts_with('*');
2713    let ends = pattern.ends_with('*');
2714    let literal = pattern
2715        .trim_start_matches('*')
2716        .trim_end_matches('*')
2717        .to_string();
2718    let contains = starts && ends;
2719    (starts, ends, contains, literal)
2720}
2721
2722/// Normalize a pair of IDs so the smaller comes first (for deduplication).
2723fn normalize_pair(a: u64, b: u64) -> (u64, u64) {
2724    if a <= b {
2725        (a, b)
2726    } else {
2727        (b, a)
2728    }
2729}
2730
2731// ============================================================================
2732// Tests
2733// ============================================================================
2734
2735#[cfg(test)]
2736mod tests {
2737    use super::*;
2738    use crate::types::{CodeUnit, CodeUnitType, Edge, EdgeType, Language, Span};
2739    use std::path::PathBuf;
2740
2741    /// Build a small test graph for query tests.
2742    fn build_test_graph() -> CodeGraph {
2743        let mut graph = CodeGraph::with_default_dimension();
2744
2745        // 0: Module "app"
2746        let m = CodeUnit::new(
2747            CodeUnitType::Module,
2748            Language::Rust,
2749            "app".to_string(),
2750            "app".to_string(),
2751            PathBuf::from("src/lib.rs"),
2752            Span::new(1, 0, 100, 0),
2753        );
2754        graph.add_unit(m);
2755
2756        // 1: Function "process"
2757        let mut f1 = CodeUnit::new(
2758            CodeUnitType::Function,
2759            Language::Rust,
2760            "process".to_string(),
2761            "app::process".to_string(),
2762            PathBuf::from("src/lib.rs"),
2763            Span::new(10, 0, 20, 0),
2764        );
2765        f1.complexity = 5;
2766        f1.visibility = Visibility::Public;
2767        graph.add_unit(f1);
2768
2769        // 2: Function "helper"
2770        let mut f2 = CodeUnit::new(
2771            CodeUnitType::Function,
2772            Language::Rust,
2773            "helper".to_string(),
2774            "app::helper".to_string(),
2775            PathBuf::from("src/lib.rs"),
2776            Span::new(25, 0, 35, 0),
2777        );
2778        f2.complexity = 2;
2779        f2.visibility = Visibility::Private;
2780        graph.add_unit(f2);
2781
2782        // 3: Test "test_process"
2783        let t = CodeUnit::new(
2784            CodeUnitType::Test,
2785            Language::Rust,
2786            "test_process".to_string(),
2787            "app::test_process".to_string(),
2788            PathBuf::from("src/lib.rs"),
2789            Span::new(40, 0, 50, 0),
2790        );
2791        graph.add_unit(t);
2792
2793        // 4: Type "Config"
2794        let ty = CodeUnit::new(
2795            CodeUnitType::Type,
2796            Language::Rust,
2797            "Config".to_string(),
2798            "app::Config".to_string(),
2799            PathBuf::from("src/lib.rs"),
2800            Span::new(55, 0, 65, 0),
2801        );
2802        graph.add_unit(ty);
2803
2804        // Edges:
2805        // app contains process, helper, test_process, Config
2806        graph.add_edge(Edge::new(0, 1, EdgeType::Contains)).ok();
2807        graph.add_edge(Edge::new(0, 2, EdgeType::Contains)).ok();
2808        graph.add_edge(Edge::new(0, 3, EdgeType::Contains)).ok();
2809        graph.add_edge(Edge::new(0, 4, EdgeType::Contains)).ok();
2810
2811        // process calls helper
2812        graph
2813            .add_edge(Edge::new(1, 2, EdgeType::Calls).with_context(15))
2814            .ok();
2815
2816        // test_process tests process
2817        graph.add_edge(Edge::new(3, 1, EdgeType::Tests)).ok();
2818
2819        graph
2820    }
2821
2822    #[test]
2823    fn test_symbol_lookup_exact() {
2824        let graph = build_test_graph();
2825        let engine = QueryEngine::new();
2826
2827        let params = SymbolLookupParams {
2828            name: "process".to_string(),
2829            mode: MatchMode::Exact,
2830            ..Default::default()
2831        };
2832
2833        let result = engine.symbol_lookup(&graph, params).expect("lookup failed");
2834        assert_eq!(result.len(), 1);
2835        assert_eq!(result[0].name, "process");
2836    }
2837
2838    #[test]
2839    fn test_symbol_lookup_prefix() {
2840        let graph = build_test_graph();
2841        let engine = QueryEngine::new();
2842
2843        let params = SymbolLookupParams {
2844            name: "proc".to_string(),
2845            mode: MatchMode::Prefix,
2846            ..Default::default()
2847        };
2848
2849        let result = engine.symbol_lookup(&graph, params).expect("lookup failed");
2850        assert_eq!(result.len(), 1);
2851    }
2852
2853    #[test]
2854    fn test_symbol_lookup_contains() {
2855        let graph = build_test_graph();
2856        let engine = QueryEngine::new();
2857
2858        let params = SymbolLookupParams {
2859            name: "elp".to_string(),
2860            mode: MatchMode::Contains,
2861            ..Default::default()
2862        };
2863
2864        let result = engine.symbol_lookup(&graph, params).expect("lookup failed");
2865        assert_eq!(result.len(), 1);
2866        assert_eq!(result[0].name, "helper");
2867    }
2868
2869    #[test]
2870    fn test_containment() {
2871        let graph = build_test_graph();
2872        let engine = QueryEngine::new();
2873
2874        let result = engine.containment(&graph, 0).expect("containment failed");
2875        assert_eq!(result.len(), 4); // process, helper, test_process, Config
2876    }
2877
2878    #[test]
2879    fn test_call_graph_callees() {
2880        let graph = build_test_graph();
2881        let engine = QueryEngine::new();
2882
2883        let params = CallGraphParams {
2884            unit_id: 1,
2885            direction: CallDirection::Callees,
2886            max_depth: 3,
2887        };
2888
2889        let result = engine
2890            .call_graph(&graph, params)
2891            .expect("call graph failed");
2892        assert!(result.nodes.len() >= 2); // process + helper
2893        assert!(!result.call_sites.is_empty());
2894    }
2895
2896    #[test]
2897    fn test_test_coverage() {
2898        let graph = build_test_graph();
2899        let engine = QueryEngine::new();
2900
2901        let result = engine.test_coverage(&graph, 1).expect("coverage failed");
2902        assert_eq!(result.direct_tests.len(), 1);
2903        assert_eq!(result.direct_tests[0], 3);
2904    }
2905
2906    #[test]
2907    fn test_shortest_path_found() {
2908        let graph = build_test_graph();
2909        let engine = QueryEngine::new();
2910
2911        let result = engine.shortest_path(&graph, 1, 2).expect("path failed");
2912        assert!(result.found);
2913        assert_eq!(result.path, vec![1, 2]);
2914        assert_eq!(result.length, 1);
2915    }
2916
2917    #[test]
2918    fn test_shortest_path_not_found() {
2919        let graph = build_test_graph();
2920        let engine = QueryEngine::new();
2921
2922        // No path from helper (2) to app (0) via forward edges.
2923        let result = engine.shortest_path(&graph, 2, 0).expect("path failed");
2924        assert!(!result.found);
2925    }
2926
2927    #[test]
2928    fn test_dependency_graph() {
2929        let graph = build_test_graph();
2930        let engine = QueryEngine::new();
2931
2932        let params = DependencyParams {
2933            unit_id: 1,
2934            max_depth: 3,
2935            edge_types: vec![EdgeType::Calls],
2936            include_transitive: true,
2937        };
2938
2939        let result = engine
2940            .dependency_graph(&graph, params)
2941            .expect("dep graph failed");
2942        assert_eq!(result.root_id, 1);
2943        assert!(!result.nodes.is_empty());
2944    }
2945
2946    #[test]
2947    fn test_reverse_dependency() {
2948        let graph = build_test_graph();
2949        let engine = QueryEngine::new();
2950
2951        let params = DependencyParams {
2952            unit_id: 2,
2953            max_depth: 3,
2954            edge_types: vec![EdgeType::Calls],
2955            include_transitive: true,
2956        };
2957
2958        let result = engine
2959            .reverse_dependency(&graph, params)
2960            .expect("rev dep failed");
2961        // process calls helper, so process should appear as a reverse dep of helper.
2962        assert!(result.nodes.iter().any(|n| n.unit_id == 1));
2963    }
2964
2965    #[test]
2966    fn test_levenshtein() {
2967        assert_eq!(levenshtein("kitten", "sitting"), 3);
2968        assert_eq!(levenshtein("", "abc"), 3);
2969        assert_eq!(levenshtein("abc", "abc"), 0);
2970        assert_eq!(levenshtein("abc", ""), 3);
2971    }
2972
2973    #[test]
2974    fn test_cosine_similarity_identical() {
2975        let a = vec![1.0, 0.0, 1.0];
2976        let b = vec![1.0, 0.0, 1.0];
2977        let sim = cosine_similarity(&a, &b);
2978        assert!((sim - 1.0).abs() < 1e-5);
2979    }
2980
2981    #[test]
2982    fn test_cosine_similarity_orthogonal() {
2983        let a = vec![1.0, 0.0];
2984        let b = vec![0.0, 1.0];
2985        let sim = cosine_similarity(&a, &b);
2986        assert!(sim.abs() < 1e-5);
2987    }
2988
2989    #[test]
2990    fn test_unit_not_found_error() {
2991        let graph = build_test_graph();
2992        let engine = QueryEngine::new();
2993
2994        let result = engine.containment(&graph, 999);
2995        assert!(result.is_err());
2996    }
2997}