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        if security.potential_issues.iter().any(|i| i.is_sensitive()) {
84            parts.push("⚠️ This query accesses potentially sensitive data.".to_string());
85        }
86
87        // Complexity notes
88        if query_info.max_depth > 3 {
89            parts.push(format!(
90                "Query has {} levels of nesting.",
91                query_info.max_depth
92            ));
93        }
94
95        // Risk level summary
96        let risk = security.assess_risk();
97        let risk_desc = match risk {
98            RiskLevel::Low => "Risk: LOW (read-only, no sensitive data)".to_string(),
99            RiskLevel::Medium => "Risk: MEDIUM (may access sensitive data)".to_string(),
100            RiskLevel::High => "Risk: HIGH (modifies multiple records)".to_string(),
101            RiskLevel::Critical => "Risk: CRITICAL (requires admin approval)".to_string(),
102        };
103        parts.push(risk_desc);
104
105        parts.join(" ")
106    }
107}
108
109/// Generate a simple description for auto-approval messages.
110#[allow(dead_code)]
111pub fn auto_approval_message(risk_level: RiskLevel) -> &'static str {
112    match risk_level {
113        RiskLevel::Low => "Auto-approved: low-risk read-only query",
114        RiskLevel::Medium => "Auto-approved: medium-risk query (configured to allow)",
115        RiskLevel::High => "Auto-approved: high-risk query (configured to allow)",
116        RiskLevel::Critical => "Auto-approved: critical-risk query (configured to allow)",
117    }
118}
119
120/// Generate a denial message when mutations are not allowed.
121#[allow(dead_code)]
122pub fn mutations_not_allowed_message() -> &'static str {
123    "Mutations are not enabled for this server. Only read-only queries are allowed in Code Mode."
124}
125
126/// Generate a message when code mode is disabled.
127#[allow(dead_code)]
128pub fn code_mode_disabled_message() -> &'static str {
129    "Code Mode is not enabled for this server. Use the standard tools instead."
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::collections::HashSet;
136
137    fn sample_query_info() -> GraphQLQueryInfo {
138        GraphQLQueryInfo {
139            operation_type: GraphQLOperationType::Query,
140            operation_name: Some("GetUsers".to_string()),
141            root_fields: vec!["users".to_string()],
142            types_accessed: {
143                let mut set = HashSet::new();
144                set.insert("User".to_string());
145                set
146            },
147            fields_accessed: {
148                let mut set = HashSet::new();
149                set.insert("id".to_string());
150                set.insert("name".to_string());
151                set.insert("email".to_string());
152                set
153            },
154            has_variables: false,
155            variable_names: vec![],
156            max_depth: 2,
157            has_fragments: false,
158            fragment_names: vec![],
159            has_introspection: false,
160        }
161    }
162
163    fn sample_security() -> SecurityAnalysis {
164        let info = sample_query_info();
165        SecurityAnalysis {
166            is_read_only: true,
167            tables_accessed: info.types_accessed,
168            fields_accessed: info.fields_accessed,
169            has_aggregation: false,
170            has_subqueries: false,
171            estimated_complexity: crate::types::Complexity::Low,
172            potential_issues: vec![],
173            estimated_rows: None,
174        }
175    }
176
177    #[test]
178    fn test_basic_explanation() {
179        let generator = TemplateExplanationGenerator::new();
180        let info = sample_query_info();
181        let security = sample_security();
182
183        let explanation = generator.explain_graphql(&info, &security);
184
185        assert!(explanation.contains("read"));
186        assert!(explanation.contains("User"));
187        assert!(explanation.contains("Risk: LOW"));
188    }
189
190    #[test]
191    fn test_mutation_explanation() {
192        let generator = TemplateExplanationGenerator::new();
193        let mut info = sample_query_info();
194        info.operation_type = GraphQLOperationType::Mutation;
195
196        let mut security = sample_security();
197        security.is_read_only = false;
198
199        let explanation = generator.explain_graphql(&info, &security);
200
201        assert!(explanation.contains("modify"));
202    }
203}