1use serde::Serialize;
2
3use plexus_engine::{
4 assess_embedded_graph_rag_capabilities, assess_embedded_graph_rag_plan, required_capabilities,
5 validate_plan_against_capabilities, CapabilityError, EngineCapabilities,
6};
7use plexus_serde::{validate_plan_structure, Plan};
8
9#[derive(Debug, Clone, Serialize)]
10pub struct ValidateReport {
11 pub structural_errors: Vec<String>,
12 pub required_ops: Vec<String>,
13 pub required_exprs: Vec<String>,
14 pub embedded_graph_rag_plan: EmbeddedGraphRagValidate,
15 pub engine_capability_gap: Option<EngineCapabilityGapReport>,
16 pub embedded_graph_rag_capability_gap: Option<EmbeddedGraphRagCapabilityGapReport>,
17}
18
19#[derive(Debug, Clone, Serialize)]
20pub struct EmbeddedGraphRagValidate {
21 pub profile: String,
22 pub supported: bool,
23 pub missing_required_ops: Vec<String>,
24 pub unsupported_ops: Vec<String>,
25 pub unsupported_exprs: Vec<String>,
26 pub graph_ref_unsupported: bool,
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct EngineCapabilityGapReport {
31 pub supported: bool,
32 pub missing_ops: Vec<String>,
33 pub missing_exprs: Vec<String>,
34 pub graph_ref_unsupported: bool,
35 pub multi_graph_unsupported: bool,
36 pub graph_param_unsupported: bool,
37 pub unsupported_plan_version: Option<String>,
38 pub raw_error: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize)]
42pub struct EmbeddedGraphRagCapabilityGapReport {
43 pub profile: String,
44 pub supported: bool,
45 pub missing_ops: Vec<String>,
46 pub missing_exprs: Vec<String>,
47}
48
49pub fn validate_plan(plan: &Plan, capabilities: Option<&EngineCapabilities>) -> ValidateReport {
50 let required = required_capabilities(plan);
51 let embedded_graph_rag_plan = assess_embedded_graph_rag_plan(plan);
52 let structural_errors = match validate_plan_structure(plan) {
53 Ok(()) => Vec::new(),
54 Err(errs) => errs.into_iter().map(|e| e.to_string()).collect(),
55 };
56
57 let (engine_capability_gap, embedded_graph_rag_capability_gap) = capabilities
58 .map(|caps| {
59 (
60 Some(capability_gap_report(plan, caps)),
61 Some(embedded_graph_rag_capability_gap_report(caps)),
62 )
63 })
64 .unwrap_or((None, None));
65
66 ValidateReport {
67 structural_errors,
68 required_ops: required
69 .required_ops
70 .iter()
71 .map(|k| k.as_str().to_string())
72 .collect(),
73 required_exprs: required
74 .required_exprs
75 .iter()
76 .map(|k| k.as_str().to_string())
77 .collect(),
78 embedded_graph_rag_plan: EmbeddedGraphRagValidate {
79 profile: embedded_graph_rag_plan.profile,
80 supported: embedded_graph_rag_plan.supported,
81 missing_required_ops: embedded_graph_rag_plan.missing_required_ops,
82 unsupported_ops: embedded_graph_rag_plan.unsupported_ops,
83 unsupported_exprs: embedded_graph_rag_plan.unsupported_exprs,
84 graph_ref_unsupported: embedded_graph_rag_plan.graph_ref_unsupported,
85 },
86 engine_capability_gap,
87 embedded_graph_rag_capability_gap,
88 }
89}
90
91fn embedded_graph_rag_capability_gap_report(
92 capabilities: &EngineCapabilities,
93) -> EmbeddedGraphRagCapabilityGapReport {
94 let gap = assess_embedded_graph_rag_capabilities(capabilities);
95 EmbeddedGraphRagCapabilityGapReport {
96 profile: gap.profile,
97 supported: gap.supported,
98 missing_ops: gap.missing_ops,
99 missing_exprs: gap.missing_exprs,
100 }
101}
102
103fn capability_gap_report(
104 plan: &Plan,
105 capabilities: &EngineCapabilities,
106) -> EngineCapabilityGapReport {
107 match validate_plan_against_capabilities(plan, capabilities) {
108 Ok(()) => EngineCapabilityGapReport {
109 supported: true,
110 missing_ops: Vec::new(),
111 missing_exprs: Vec::new(),
112 graph_ref_unsupported: false,
113 multi_graph_unsupported: false,
114 graph_param_unsupported: false,
115 unsupported_plan_version: None,
116 raw_error: None,
117 },
118 Err(error) => capability_error_to_gap(error),
119 }
120}
121
122fn capability_error_to_gap(error: CapabilityError) -> EngineCapabilityGapReport {
123 match error {
124 CapabilityError::MissingFeatureSupport {
125 missing_ops,
126 missing_exprs,
127 } => EngineCapabilityGapReport {
128 supported: false,
129 missing_ops: missing_ops
130 .into_iter()
131 .map(|op| op.as_str().to_string())
132 .collect(),
133 missing_exprs: missing_exprs
134 .into_iter()
135 .map(|expr| expr.as_str().to_string())
136 .collect(),
137 graph_ref_unsupported: false,
138 multi_graph_unsupported: false,
139 graph_param_unsupported: false,
140 unsupported_plan_version: None,
141 raw_error: None,
142 },
143 CapabilityError::GraphRefUnsupported => EngineCapabilityGapReport {
144 supported: false,
145 missing_ops: Vec::new(),
146 missing_exprs: Vec::new(),
147 graph_ref_unsupported: true,
148 multi_graph_unsupported: false,
149 graph_param_unsupported: false,
150 unsupported_plan_version: None,
151 raw_error: None,
152 },
153 CapabilityError::MultiGraphUnsupported => EngineCapabilityGapReport {
154 supported: false,
155 missing_ops: Vec::new(),
156 missing_exprs: Vec::new(),
157 graph_ref_unsupported: false,
158 multi_graph_unsupported: true,
159 graph_param_unsupported: false,
160 unsupported_plan_version: None,
161 raw_error: None,
162 },
163 CapabilityError::GraphParamUnsupported => EngineCapabilityGapReport {
164 supported: false,
165 missing_ops: Vec::new(),
166 missing_exprs: Vec::new(),
167 graph_ref_unsupported: false,
168 multi_graph_unsupported: false,
169 graph_param_unsupported: true,
170 unsupported_plan_version: None,
171 raw_error: None,
172 },
173 CapabilityError::UnsupportedPlanVersion {
174 plan_major,
175 plan_minor,
176 plan_patch,
177 min_major,
178 min_minor,
179 min_patch,
180 max_major,
181 max_minor,
182 max_patch,
183 } => EngineCapabilityGapReport {
184 supported: false,
185 missing_ops: Vec::new(),
186 missing_exprs: Vec::new(),
187 graph_ref_unsupported: false,
188 multi_graph_unsupported: false,
189 graph_param_unsupported: false,
190 unsupported_plan_version: Some(format!(
191 "plan {plan_major}.{plan_minor}.{plan_patch}; supported {min_major}.{min_minor}.{min_patch}..={max_major}.{max_minor}.{max_patch}"
192 )),
193 raw_error: None,
194 },
195 other => EngineCapabilityGapReport {
196 supported: false,
197 missing_ops: Vec::new(),
198 missing_exprs: Vec::new(),
199 graph_ref_unsupported: false,
200 multi_graph_unsupported: false,
201 graph_param_unsupported: false,
202 unsupported_plan_version: None,
203 raw_error: Some(other.to_string()),
204 },
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use plexus_engine::{ExprKind, OpKind, PlanSemver, VersionRange};
212 use plexus_serde::{
213 current_plan_version, ColDef, ColKind, Expr, LogicalType, Op, Plan, VectorMetric,
214 };
215
216 fn embedded_plan() -> Plan {
217 Plan {
218 version: current_plan_version("test"),
219 ops: vec![
220 Op::ConstRow,
221 Op::VectorScan {
222 input: 0,
223 collection: "docs".to_string(),
224 query_vector: Expr::ListLiteral {
225 items: vec![Expr::FloatLiteral(1.0), Expr::FloatLiteral(0.0)],
226 },
227 metric: VectorMetric::DotProduct,
228 top_k: 8,
229 approx_hint: false,
230 schema: vec![
231 ColDef {
232 name: "doc".to_string(),
233 kind: ColKind::Node,
234 logical_type: LogicalType::Unknown,
235 },
236 ColDef {
237 name: "score".to_string(),
238 kind: ColKind::Float64,
239 logical_type: LogicalType::Float64,
240 },
241 ],
242 },
243 Op::Rerank {
244 input: 1,
245 score_expr: Expr::VectorSimilarity {
246 metric: VectorMetric::DotProduct,
247 lhs: Box::new(Expr::PropAccess {
248 col: 0,
249 prop: "embedding".to_string(),
250 }),
251 rhs: Box::new(Expr::ListLiteral {
252 items: vec![Expr::FloatLiteral(1.0), Expr::FloatLiteral(0.0)],
253 }),
254 },
255 top_k: 4,
256 schema: vec![
257 ColDef {
258 name: "doc".to_string(),
259 kind: ColKind::Node,
260 logical_type: LogicalType::Unknown,
261 },
262 ColDef {
263 name: "score".to_string(),
264 kind: ColKind::Float64,
265 logical_type: LogicalType::Float64,
266 },
267 ],
268 },
269 Op::Return { input: 2 },
270 ],
271 root_op: 3,
272 }
273 }
274
275 #[test]
276 fn validate_reports_capability_gaps() {
277 let plan = embedded_plan();
278 let mut caps = EngineCapabilities::full(VersionRange::new(
279 PlanSemver::new(0, 3, 0),
280 PlanSemver::new(0, 3, 0),
281 ));
282 caps.supported_ops.remove(&OpKind::Rerank);
283 caps.supported_exprs.remove(&ExprKind::VectorSimilarity);
284
285 let report = validate_plan(&plan, Some(&caps));
286 let engine_gap = report
287 .engine_capability_gap
288 .expect("engine capability gap report");
289 assert!(!engine_gap.supported);
290 assert_eq!(engine_gap.missing_ops, vec!["Rerank"]);
291 assert_eq!(engine_gap.missing_exprs, vec!["VectorSimilarity"]);
292
293 let embedded_gap = report
294 .embedded_graph_rag_capability_gap
295 .expect("embedded profile capability gap");
296 assert_eq!(embedded_gap.missing_ops, vec!["Rerank"]);
297 assert_eq!(embedded_gap.missing_exprs, vec!["VectorSimilarity"]);
298 }
299}