1use ainl_contracts::{
4 RepoIntelCapabilityProfile, RepoIntelCapabilityState, RepoIntelToolClass,
5 CONTRACT_SCHEMA_VERSION,
6};
7use std::collections::{HashMap, HashSet};
8
9pub const CHECK_ID_REPO_INTELLIGENCE: &str = "repo_intelligence";
11
12#[derive(Debug, Clone)]
14pub struct McpToolRow {
15 pub server_name: String,
16 pub tool_name: String,
17 pub description: String,
18}
19
20#[must_use]
22pub fn tool_class_matches(tool_name: &str, description: &str, class: RepoIntelToolClass) -> bool {
23 let n = format!("{} {}", tool_name, description).to_ascii_lowercase();
24 match class {
25 RepoIntelToolClass::Query => {
26 n.contains("query")
27 && (n.contains("search") || n.contains("hybrid") || n.contains("bm25"))
28 || n.contains("gitnexus") && n.contains("query")
29 || n.ends_with("query")
30 }
31 RepoIntelToolClass::Context => {
32 n.contains("context") && n.contains("symbol")
33 || n.contains("360")
34 || n.contains("callee")
35 || n.contains("caller")
36 }
37 RepoIntelToolClass::Impact => {
38 n.contains("impact") || n.contains("blast") || n.contains("radius")
39 }
40 RepoIntelToolClass::DetectChanges => {
41 n.contains("detect_changes")
42 || n.contains("detectchanges")
43 || (n.contains("diff") && n.contains("impact"))
44 }
45 RepoIntelToolClass::Cypher => n.contains("cypher"),
46 }
47}
48
49#[must_use]
51pub fn classify_inventory(rows: &[McpToolRow]) -> RepoIntelCapabilityProfile {
52 let mut classes: HashSet<RepoIntelToolClass> = HashSet::new();
53 for row in rows {
54 for c in [
55 RepoIntelToolClass::Query,
56 RepoIntelToolClass::Context,
57 RepoIntelToolClass::Impact,
58 RepoIntelToolClass::DetectChanges,
59 RepoIntelToolClass::Cypher,
60 ] {
61 if tool_class_matches(&row.tool_name, &row.description, c) {
62 classes.insert(c);
63 }
64 }
65 }
66
67 let mut class_vec: Vec<RepoIntelToolClass> = classes.into_iter().collect();
68 class_vec.sort_by_key(|c| format!("{c:?}"));
69
70 let has_impact = class_vec.contains(&RepoIntelToolClass::Impact);
71 let has_query_or_ctx = class_vec.contains(&RepoIntelToolClass::Query)
72 || class_vec.contains(&RepoIntelToolClass::Context);
73
74 let state = if has_impact && has_query_or_ctx {
75 RepoIntelCapabilityState::Ready
76 } else if !class_vec.is_empty() {
77 RepoIntelCapabilityState::Degraded
78 } else {
79 RepoIntelCapabilityState::Absent
80 };
81
82 let note = match state {
83 RepoIntelCapabilityState::Ready => None,
84 RepoIntelCapabilityState::Degraded => Some(
85 "Some repo-intelligence tools detected; prefer impact+context or impact+query for full workflow."
86 .into(),
87 ),
88 RepoIntelCapabilityState::Absent => Some(
89 "No repo-intelligence MCP tools detected (query/context/impact). Install a GitNexus-class indexer or similar."
90 .into(),
91 ),
92 };
93
94 RepoIntelCapabilityProfile {
95 schema_version: CONTRACT_SCHEMA_VERSION,
96 state,
97 classes_present: class_vec,
98 note,
99 }
100}
101
102#[derive(Debug, Clone, serde::Serialize)]
104pub struct ServerRepoIntelSummary {
105 pub server_name: String,
106 pub profile: RepoIntelCapabilityProfile,
107}
108
109#[must_use]
110pub fn summarize_per_server(rows: &[McpToolRow]) -> Vec<ServerRepoIntelSummary> {
111 let mut by_server: HashMap<String, Vec<McpToolRow>> = HashMap::new();
112 for r in rows {
113 by_server
114 .entry(r.server_name.clone())
115 .or_default()
116 .push(r.clone());
117 }
118 let mut names: Vec<_> = by_server.keys().cloned().collect();
119 names.sort();
120 names
121 .into_iter()
122 .map(|name| ServerRepoIntelSummary {
123 profile: classify_inventory(by_server.get(&name).map(|v| v.as_slice()).unwrap_or(&[])),
124 server_name: name,
125 })
126 .collect()
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn gitnex_class_like_tools_ready() {
135 let rows = vec![
136 McpToolRow {
137 server_name: "gitnexus".into(),
138 tool_name: "mcp_gitnexus_query".into(),
139 description: "Hybrid search".into(),
140 },
141 McpToolRow {
142 server_name: "gitnexus".into(),
143 tool_name: "mcp_gitnexus_impact".into(),
144 description: "Blast radius".into(),
145 },
146 ];
147 let p = classify_inventory(&rows);
148 assert_eq!(p.state, RepoIntelCapabilityState::Ready);
149 }
150}