Skip to main content

pmcp_code_mode/
explanation.rs

1//! Template-based explanation generation for Code Mode.
2//!
3//! MVP uses templates. Full implementation will use server-side LLM.
4
5use crate::graphql::{GraphQLOperationType, GraphQLQueryInfo};
6use crate::types::{RiskLevel, SecurityAnalysis};
7
8/// Trait for generating human-readable explanations.
9pub trait ExplanationGenerator: Send + Sync {
10    /// Generate an explanation for a GraphQL query.
11    fn explain_graphql(&self, query_info: &GraphQLQueryInfo, security: &SecurityAnalysis)
12        -> String;
13}
14
15/// Template-based explanation generator for MVP.
16pub struct TemplateExplanationGenerator;
17
18impl Default for TemplateExplanationGenerator {
19    fn default() -> Self {
20        Self
21    }
22}
23
24impl TemplateExplanationGenerator {
25    pub fn new() -> Self {
26        Self
27    }
28}
29
30impl ExplanationGenerator for TemplateExplanationGenerator {
31    fn explain_graphql(
32        &self,
33        query_info: &GraphQLQueryInfo,
34        security: &SecurityAnalysis,
35    ) -> String {
36        let mut parts = Vec::new();
37
38        // Operation type description
39        let op_desc = match query_info.operation_type {
40            GraphQLOperationType::Query => "This query will read",
41            GraphQLOperationType::Mutation => "This mutation will modify",
42            GraphQLOperationType::Subscription => "This subscription will watch",
43        };
44
45        // What data is being accessed
46        let types: Vec<&str> = security
47            .tables_accessed
48            .iter()
49            .map(|s| s.as_str())
50            .collect();
51        let types_desc = if types.is_empty() {
52            "data".to_string()
53        } else if types.len() == 1 {
54            format!("{} data", types[0])
55        } else {
56            let last = types.last().unwrap();
57            let rest = &types[..types.len() - 1];
58            format!("{} and {} data", rest.join(", "), last)
59        };
60
61        parts.push(format!("{} {}.", op_desc, types_desc));
62
63        // Fields being accessed
64        let fields: Vec<&str> = security
65            .fields_accessed
66            .iter()
67            .map(|s| s.as_str())
68            .collect();
69        if !fields.is_empty() {
70            let field_count = fields.len();
71            if field_count <= 5 {
72                parts.push(format!("Fields: {}.", fields.join(", ")));
73            } else {
74                parts.push(format!(
75                    "Accessing {} fields including: {}.",
76                    field_count,
77                    fields[..5].join(", ")
78                ));
79            }
80        }
81
82        // Security warnings
83        let sensitive_issues: Vec<_> = security
84            .potential_issues
85            .iter()
86            .filter(|i| i.is_sensitive())
87            .collect();
88
89        if !sensitive_issues.is_empty() {
90            parts.push("⚠️ This query accesses potentially sensitive data.".to_string());
91        }
92
93        // Complexity notes
94        if query_info.max_depth > 3 {
95            parts.push(format!(
96                "Query has {} levels of nesting.",
97                query_info.max_depth
98            ));
99        }
100
101        // Risk level summary
102        let risk = security.assess_risk();
103        let risk_desc = match risk {
104            RiskLevel::Low => "Risk: LOW (read-only, no sensitive data)".to_string(),
105            RiskLevel::Medium => "Risk: MEDIUM (may access sensitive data)".to_string(),
106            RiskLevel::High => "Risk: HIGH (modifies multiple records)".to_string(),
107            RiskLevel::Critical => "Risk: CRITICAL (requires admin approval)".to_string(),
108        };
109        parts.push(risk_desc);
110
111        parts.join(" ")
112    }
113}
114
115/// Generate a simple description for auto-approval messages.
116#[allow(dead_code)]
117pub fn auto_approval_message(risk_level: RiskLevel) -> &'static str {
118    match risk_level {
119        RiskLevel::Low => "Auto-approved: low-risk read-only query",
120        RiskLevel::Medium => "Auto-approved: medium-risk query (configured to allow)",
121        RiskLevel::High => "Auto-approved: high-risk query (configured to allow)",
122        RiskLevel::Critical => "Auto-approved: critical-risk query (configured to allow)",
123    }
124}
125
126/// Generate a denial message when mutations are not allowed.
127#[allow(dead_code)]
128pub fn mutations_not_allowed_message() -> &'static str {
129    "Mutations are not enabled for this server. Only read-only queries are allowed in Code Mode."
130}
131
132/// Generate a message when code mode is disabled.
133#[allow(dead_code)]
134pub fn code_mode_disabled_message() -> &'static str {
135    "Code Mode is not enabled for this server. Use the standard tools instead."
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::collections::HashSet;
142
143    fn sample_query_info() -> GraphQLQueryInfo {
144        GraphQLQueryInfo {
145            operation_type: GraphQLOperationType::Query,
146            operation_name: Some("GetUsers".to_string()),
147            root_fields: vec!["users".to_string()],
148            types_accessed: {
149                let mut set = HashSet::new();
150                set.insert("User".to_string());
151                set
152            },
153            fields_accessed: {
154                let mut set = HashSet::new();
155                set.insert("id".to_string());
156                set.insert("name".to_string());
157                set.insert("email".to_string());
158                set
159            },
160            has_variables: false,
161            variable_names: vec![],
162            max_depth: 2,
163            has_fragments: false,
164            fragment_names: vec![],
165            has_introspection: false,
166        }
167    }
168
169    fn sample_security() -> SecurityAnalysis {
170        let info = sample_query_info();
171        SecurityAnalysis {
172            is_read_only: true,
173            tables_accessed: info.types_accessed,
174            fields_accessed: info.fields_accessed,
175            has_aggregation: false,
176            has_subqueries: false,
177            estimated_complexity: crate::types::Complexity::Low,
178            potential_issues: vec![],
179            estimated_rows: None,
180        }
181    }
182
183    #[test]
184    fn test_basic_explanation() {
185        let generator = TemplateExplanationGenerator::new();
186        let info = sample_query_info();
187        let security = sample_security();
188
189        let explanation = generator.explain_graphql(&info, &security);
190
191        assert!(explanation.contains("read"));
192        assert!(explanation.contains("User"));
193        assert!(explanation.contains("Risk: LOW"));
194    }
195
196    #[test]
197    fn test_mutation_explanation() {
198        let generator = TemplateExplanationGenerator::new();
199        let mut info = sample_query_info();
200        info.operation_type = GraphQLOperationType::Mutation;
201
202        let mut security = sample_security();
203        security.is_read_only = false;
204
205        let explanation = generator.explain_graphql(&info, &security);
206
207        assert!(explanation.contains("modify"));
208    }
209}