1use std::collections::{HashMap, HashSet};
43
44use serde::{Deserialize, Serialize};
45
46use crate::learn::offline::LearnedActionOrder;
47use crate::types::LoraConfig;
48
49#[derive(Debug, Clone)]
57pub struct VotingStrategy {
58 pub high_threshold: f64,
60 pub medium_threshold: f64,
62}
63
64impl Default for VotingStrategy {
65 fn default() -> Self {
66 Self {
67 high_threshold: 0.8,
68 medium_threshold: 0.6,
69 }
70 }
71}
72
73impl VotingStrategy {
74 pub fn determine(&self, match_rate: f64, has_lora: bool) -> u8 {
83 if match_rate >= 1.0 {
84 0 } else if match_rate >= self.high_threshold && has_lora {
86 1 } else {
88 3 }
90 }
91}
92
93#[derive(Debug, Clone)]
99pub enum SelectResult {
100 UseLearnedGraph {
102 graph: Box<DependencyGraph>,
104 lora: Option<LoraConfig>,
106 },
107
108 UseLlm {
110 lora: Option<LoraConfig>,
112 hint: Option<LearnedActionOrder>,
114 vote_count: u8,
116 match_rate: f64,
118 },
119}
120
121impl SelectResult {
122 pub fn needs_llm(&self) -> bool {
124 matches!(self, SelectResult::UseLlm { .. })
125 }
126
127 pub fn vote_count(&self) -> u8 {
129 match self {
130 SelectResult::UseLearnedGraph { .. } => 0,
131 SelectResult::UseLlm { vote_count, .. } => *vote_count,
132 }
133 }
134
135 pub fn lora(&self) -> Option<&LoraConfig> {
137 match self {
138 SelectResult::UseLearnedGraph { lora, .. } => lora.as_ref(),
139 SelectResult::UseLlm { lora, .. } => lora.as_ref(),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct DependencyEdge {
154 pub from: String,
156 pub to: String,
158 pub confidence: f64,
160}
161
162impl DependencyEdge {
163 pub fn new(from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
164 Self {
165 from: from.into(),
166 to: to.into(),
167 confidence: confidence.clamp(0.0, 1.0),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct DependencyGraph {
178 edges: Vec<DependencyEdge>,
180 start_nodes: HashSet<String>,
182 terminal_nodes: HashSet<String>,
184 task: String,
186 available_actions: Vec<String>,
188 #[serde(default)]
192 param_variants: HashMap<String, (String, Vec<String>)>,
193 #[serde(default)]
195 discover_order: Vec<String>,
196 #[serde(default)]
198 not_discover_order: Vec<String>,
199 #[serde(skip)]
201 learn_record: Option<crate::learn::DependencyGraphRecord>,
202}
203
204impl DependencyGraph {
205 pub fn new() -> Self {
207 Self::default()
208 }
209
210 pub fn builder() -> DependencyGraphBuilder {
212 DependencyGraphBuilder::new()
213 }
214
215 pub fn valid_next_actions(&self, current_action: &str) -> Vec<String> {
223 let mut edges: Vec<_> = self
224 .edges
225 .iter()
226 .filter(|e| e.from == current_action)
227 .collect();
228
229 edges.sort_by(|a, b| {
231 b.confidence
232 .partial_cmp(&a.confidence)
233 .unwrap_or(std::cmp::Ordering::Equal)
234 });
235
236 edges.iter().map(|e| e.to.clone()).collect()
237 }
238
239 pub fn start_actions(&self) -> Vec<String> {
243 let mut actions: Vec<_> = self.start_nodes.iter().cloned().collect();
244 actions.sort();
245 actions
246 }
247
248 pub fn terminal_actions(&self) -> Vec<String> {
252 let mut actions: Vec<_> = self.terminal_nodes.iter().cloned().collect();
253 actions.sort();
254 actions
255 }
256
257 pub fn is_terminal(&self, action: &str) -> bool {
259 self.terminal_nodes.contains(action)
260 }
261
262 pub fn is_start(&self, action: &str) -> bool {
264 self.start_nodes.contains(action)
265 }
266
267 pub fn can_transition(&self, from: &str, to: &str) -> bool {
269 self.edges.iter().any(|e| e.from == from && e.to == to)
270 }
271
272 pub fn transition_confidence(&self, from: &str, to: &str) -> Option<f64> {
274 self.edges
275 .iter()
276 .find(|e| e.from == from && e.to == to)
277 .map(|e| e.confidence)
278 }
279
280 pub fn edges(&self) -> &[DependencyEdge] {
282 &self.edges
283 }
284
285 pub fn task(&self) -> &str {
287 &self.task
288 }
289
290 pub fn available_actions(&self) -> &[String] {
292 &self.available_actions
293 }
294
295 pub fn param_variants(&self, action: &str) -> Option<(&str, &[String])> {
297 self.param_variants
298 .get(action)
299 .map(|(key, values)| (key.as_str(), values.as_slice()))
300 }
301
302 pub fn all_param_variants(&self) -> &HashMap<String, (String, Vec<String>)> {
304 &self.param_variants
305 }
306
307 pub fn discover_order(&self) -> &[String] {
313 &self.discover_order
314 }
315
316 pub fn not_discover_order(&self) -> &[String] {
318 &self.not_discover_order
319 }
320
321 pub fn set_action_order(&mut self, discover: Vec<String>, not_discover: Vec<String>) {
323 self.discover_order = discover;
324 self.not_discover_order = not_discover;
325 }
326
327 pub fn has_action_order(&self) -> bool {
329 !self.discover_order.is_empty() || !self.not_discover_order.is_empty()
330 }
331
332 pub fn set_learn_record(&mut self, record: crate::learn::DependencyGraphRecord) {
338 self.learn_record = Some(record);
339 }
340
341 pub fn learn_record(&self) -> Option<&crate::learn::DependencyGraphRecord> {
343 self.learn_record.as_ref()
344 }
345
346 pub fn take_learn_record(&mut self) -> Option<crate::learn::DependencyGraphRecord> {
348 self.learn_record.take()
349 }
350
351 pub fn validate(&self) -> Result<(), DependencyGraphError> {
363 if self.start_nodes.is_empty() {
364 return Err(DependencyGraphError::NoStartNodes);
365 }
366
367 if self.terminal_nodes.is_empty() {
368 return Err(DependencyGraphError::NoTerminalNodes);
369 }
370
371 for node in &self.start_nodes {
373 if !self.available_actions.contains(node) {
374 return Err(DependencyGraphError::UnknownAction(node.clone()));
375 }
376 }
377
378 for node in &self.terminal_nodes {
380 if !self.available_actions.contains(node) {
381 return Err(DependencyGraphError::UnknownAction(node.clone()));
382 }
383 }
384
385 for edge in &self.edges {
387 if !self.available_actions.contains(&edge.from) {
388 return Err(DependencyGraphError::UnknownAction(edge.from.clone()));
389 }
390 if !self.available_actions.contains(&edge.to) {
391 return Err(DependencyGraphError::UnknownAction(edge.to.clone()));
392 }
393 }
394
395 Ok(())
396 }
397
398 pub fn to_mermaid(&self) -> String {
404 let mut lines = vec!["graph LR".to_string()];
405
406 for edge in &self.edges {
407 let label = format!("{:.0}%", edge.confidence * 100.0);
408 lines.push(format!(" {} -->|{}| {}", edge.from, label, edge.to));
409 }
410
411 for start in &self.start_nodes {
413 lines.push(format!(" style {} fill:#9f9", start));
414 }
415
416 for terminal in &self.terminal_nodes {
418 lines.push(format!(" style {} fill:#f99", terminal));
419 }
420
421 lines.join("\n")
422 }
423}
424
425#[derive(Debug, Clone, thiserror::Error)]
427pub enum DependencyGraphError {
428 #[error("No start nodes defined")]
429 NoStartNodes,
430
431 #[error("No terminal nodes defined")]
432 NoTerminalNodes,
433
434 #[error("Unknown action: {0}")]
435 UnknownAction(String),
436
437 #[error("Parse error: {0}")]
438 ParseError(String),
439
440 #[error("LLM error: {0}")]
441 LlmError(String),
442}
443
444pub trait DependencyGraphProvider: Send + Sync {
471 fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph>;
481
482 fn select(&self, _task: &str, _available_actions: &[String]) -> Option<SelectResult> {
491 None
492 }
493}
494
495#[derive(Debug, Clone, Default)]
501pub struct DependencyGraphBuilder {
502 edges: Vec<DependencyEdge>,
503 start_nodes: HashSet<String>,
504 terminal_nodes: HashSet<String>,
505 task: String,
506 available_actions: Vec<String>,
507 param_variants: HashMap<String, (String, Vec<String>)>,
508 discover_order: Vec<String>,
509 not_discover_order: Vec<String>,
510}
511
512impl DependencyGraphBuilder {
513 pub fn new() -> Self {
514 Self::default()
515 }
516
517 pub fn task(mut self, task: impl Into<String>) -> Self {
519 self.task = task.into();
520 self
521 }
522
523 pub fn available_actions<I, S>(mut self, actions: I) -> Self
525 where
526 I: IntoIterator<Item = S>,
527 S: Into<String>,
528 {
529 self.available_actions = actions.into_iter().map(|s| s.into()).collect();
530 self
531 }
532
533 pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>, confidence: f64) -> Self {
535 self.edges.push(DependencyEdge::new(from, to, confidence));
536 self
537 }
538
539 pub fn start_node(mut self, action: impl Into<String>) -> Self {
541 self.start_nodes.insert(action.into());
542 self
543 }
544
545 pub fn start_nodes<I, S>(mut self, actions: I) -> Self
547 where
548 I: IntoIterator<Item = S>,
549 S: Into<String>,
550 {
551 self.start_nodes
552 .extend(actions.into_iter().map(|s| s.into()));
553 self
554 }
555
556 pub fn terminal_node(mut self, action: impl Into<String>) -> Self {
558 self.terminal_nodes.insert(action.into());
559 self
560 }
561
562 pub fn terminal_nodes<I, S>(mut self, actions: I) -> Self
564 where
565 I: IntoIterator<Item = S>,
566 S: Into<String>,
567 {
568 self.terminal_nodes
569 .extend(actions.into_iter().map(|s| s.into()));
570 self
571 }
572
573 pub fn param_variants<I, S>(
577 mut self,
578 action: impl Into<String>,
579 key: impl Into<String>,
580 values: I,
581 ) -> Self
582 where
583 I: IntoIterator<Item = S>,
584 S: Into<String>,
585 {
586 self.param_variants.insert(
587 action.into(),
588 (key.into(), values.into_iter().map(|s| s.into()).collect()),
589 );
590 self
591 }
592
593 pub fn with_orders(
597 mut self,
598 discover_order: Vec<String>,
599 not_discover_order: Vec<String>,
600 ) -> Self {
601 self.discover_order = discover_order;
602 self.not_discover_order = not_discover_order;
603 self
604 }
605
606 pub fn build(self) -> DependencyGraph {
608 DependencyGraph {
609 edges: self.edges,
610 start_nodes: self.start_nodes,
611 terminal_nodes: self.terminal_nodes,
612 task: self.task,
613 available_actions: self.available_actions,
614 param_variants: self.param_variants,
615 discover_order: self.discover_order,
616 not_discover_order: self.not_discover_order,
617 learn_record: None,
618 }
619 }
620
621 pub fn build_validated(self) -> Result<DependencyGraph, DependencyGraphError> {
623 let graph = self.build();
624 graph.validate()?;
625 Ok(graph)
626 }
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
637pub struct LlmDependencyResponse {
638 pub edges: Vec<LlmEdge>,
640 pub start: Vec<String>,
642 pub terminal: Vec<String>,
644 #[serde(default)]
646 pub reasoning: Option<String>,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct LlmEdge {
652 pub from: String,
653 pub to: String,
654 pub confidence: f64,
655}
656
657impl LlmDependencyResponse {
658 pub fn into_graph(
660 self,
661 task: impl Into<String>,
662 available_actions: Vec<String>,
663 ) -> DependencyGraph {
664 let mut builder = DependencyGraphBuilder::new()
665 .task(task)
666 .available_actions(available_actions)
667 .start_nodes(self.start)
668 .terminal_nodes(self.terminal);
669
670 for edge in self.edges {
671 builder = builder.edge(edge.from, edge.to, edge.confidence);
672 }
673
674 builder.build()
675 }
676
677 pub fn parse(text: &str) -> Result<Self, DependencyGraphError> {
683 if let Some(response) = Self::parse_arrow_format(text) {
685 return Ok(response);
686 }
687
688 if let Ok(parsed) = serde_json::from_str(text) {
690 return Ok(parsed);
691 }
692
693 if let Some(json) = Self::extract_json(text) {
695 serde_json::from_str(&json).map_err(|e| DependencyGraphError::ParseError(e.to_string()))
696 } else {
697 Err(DependencyGraphError::ParseError(format!(
698 "No valid format found in response: {}",
699 text.chars().take(200).collect::<String>()
700 )))
701 }
702 }
703
704 fn parse_arrow_format(text: &str) -> Option<Self> {
711 if let Some(result) = Self::parse_arrow_only(text) {
713 return Some(result);
714 }
715
716 if let Some(result) = Self::parse_numbered_list(text) {
718 return Some(result);
719 }
720
721 None
722 }
723
724 fn parse_arrow_only(text: &str) -> Option<Self> {
726 let normalized = text.replace('→', "->");
727
728 let arrow_line = normalized.lines().find(|line| line.contains("->"))?;
730
731 let parts: Vec<&str> = arrow_line.split("->").collect();
732 if parts.len() < 2 {
733 return None;
734 }
735
736 let actions_in_order: Vec<String> = parts
738 .iter()
739 .filter_map(|part| {
740 let trimmed = part.trim();
741 let last_word = trimmed.split_whitespace().last()?;
743 let action: String = last_word.chars().filter(|c| c.is_alphabetic()).collect();
745 if action.is_empty() {
746 None
747 } else {
748 Some(action)
749 }
750 })
751 .collect();
752
753 if actions_in_order.len() < 2 {
754 return None;
755 }
756
757 Self::build_response(actions_in_order)
758 }
759
760 fn parse_numbered_list(text: &str) -> Option<Self> {
762 let mut actions_in_order: Vec<String> = Vec::new();
763
764 for i in 1..=10 {
766 let pattern = format!("{}.", i);
767 if let Some(pos) = text.find(&pattern) {
768 let after = &text[pos + pattern.len()..];
770 if let Some(word) = after.split_whitespace().next() {
771 let action: String = word.chars().filter(|c| c.is_alphabetic()).collect();
773 if !action.is_empty() && !actions_in_order.contains(&action) {
774 actions_in_order.push(action);
775 }
776 }
777 }
778 }
779
780 if actions_in_order.len() < 2 {
781 return None;
782 }
783
784 Self::build_response(actions_in_order)
785 }
786
787 fn build_response(actions_in_order: Vec<String>) -> Option<Self> {
789 let mut edges = Vec::new();
790 for window in actions_in_order.windows(2) {
791 edges.push(LlmEdge {
792 from: window[0].clone(),
793 to: window[1].clone(),
794 confidence: 0.9,
795 });
796 }
797
798 Some(Self {
799 edges,
800 start: vec![actions_in_order.first()?.clone()],
801 terminal: vec![actions_in_order.last()?.clone()],
802 reasoning: Some("Parsed from text format".to_string()),
803 })
804 }
805
806 fn extract_json(text: &str) -> Option<String> {
808 let start = text.find('{')?;
810 let chars: Vec<char> = text[start..].chars().collect();
811 let mut depth = 0;
812 let mut in_string = false;
813 let mut escape_next = false;
814
815 for (i, &ch) in chars.iter().enumerate() {
816 if escape_next {
817 escape_next = false;
818 continue;
819 }
820
821 match ch {
822 '\\' if in_string => escape_next = true,
823 '"' => in_string = !in_string,
824 '{' if !in_string => depth += 1,
825 '}' if !in_string => {
826 depth -= 1;
827 if depth == 0 {
828 return Some(chars[..=i].iter().collect());
829 }
830 }
831 _ => {}
832 }
833 }
834
835 None
836 }
837}
838
839pub trait DependencyPlanner: Send + Sync {
847 fn plan(
851 &self,
852 task: &str,
853 available_actions: &[String],
854 ) -> Result<DependencyGraph, DependencyGraphError>;
855
856 fn name(&self) -> &str;
858}
859
860#[derive(Debug, Clone, Default)]
865pub struct StaticDependencyPlanner {
866 patterns: HashMap<String, DependencyGraph>,
868 default_pattern: Option<String>,
870}
871
872impl StaticDependencyPlanner {
873 pub fn new() -> Self {
874 Self::default()
875 }
876
877 pub fn with_pattern(mut self, name: impl Into<String>, graph: DependencyGraph) -> Self {
879 let name = name.into();
880 if self.default_pattern.is_none() {
881 self.default_pattern = Some(name.clone());
882 }
883 self.patterns.insert(name, graph);
884 self
885 }
886
887 pub fn with_default_pattern(mut self, name: impl Into<String>) -> Self {
889 self.default_pattern = Some(name.into());
890 self
891 }
892
893 pub fn with_file_exploration_pattern(self) -> Self {
897 let graph = DependencyGraph::builder()
898 .task("File exploration")
899 .available_actions(["Grep", "List", "Read"])
900 .edge("Grep", "Read", 0.95)
901 .edge("List", "Grep", 0.60)
902 .edge("List", "Read", 0.40)
903 .start_nodes(["Grep", "List"])
904 .terminal_node("Read")
905 .build();
906
907 self.with_pattern("file_exploration", graph)
908 }
909
910 pub fn with_code_search_pattern(self) -> Self {
914 let graph = DependencyGraph::builder()
915 .task("Code search")
916 .available_actions(["Grep", "Read"])
917 .edge("Grep", "Read", 0.95)
918 .start_node("Grep")
919 .terminal_node("Read")
920 .build();
921
922 self.with_pattern("code_search", graph)
923 }
924}
925
926impl DependencyPlanner for StaticDependencyPlanner {
927 fn plan(
928 &self,
929 task: &str,
930 available_actions: &[String],
931 ) -> Result<DependencyGraph, DependencyGraphError> {
932 if let Some(pattern_name) = &self.default_pattern {
934 if let Some(graph) = self.patterns.get(pattern_name) {
935 let mut graph = graph.clone();
936 graph.task = task.to_string();
937 graph.available_actions = available_actions.to_vec();
938 return Ok(graph);
939 }
940 }
941
942 if available_actions.is_empty() {
945 return Err(DependencyGraphError::NoStartNodes);
946 }
947
948 let mut builder = DependencyGraphBuilder::new()
949 .task(task)
950 .available_actions(available_actions.to_vec())
951 .start_node(&available_actions[0]);
952
953 if available_actions.len() > 1 {
954 for window in available_actions.windows(2) {
955 builder = builder.edge(&window[0], &window[1], 0.80);
956 }
957 builder = builder.terminal_node(&available_actions[available_actions.len() - 1]);
958 } else {
959 builder = builder.terminal_node(&available_actions[0]);
960 }
961
962 Ok(builder.build())
963 }
964
965 fn name(&self) -> &str {
966 "StaticDependencyPlanner"
967 }
968}
969
970use crate::actions::ActionDef;
975
976pub struct DependencyPromptGenerator;
981
982impl DependencyPromptGenerator {
983 pub fn generate_prompt(task: &str, actions: &[ActionDef]) -> String {
987 let actions_list = actions
988 .iter()
989 .map(|a| a.name.as_str())
990 .collect::<Vec<_>>()
991 .join(", ");
992
993 format!(
994 r#"{task}
995Steps: {actions_list}
996The very first step is:"#
997 )
998 }
999
1000 pub fn generate_first_prompt(_task: &str, actions: &[ActionDef]) -> String {
1005 let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
1007 sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
1008
1009 let actions_list = sorted_actions
1010 .iter()
1011 .map(|a| a.name.as_str())
1012 .collect::<Vec<_>>()
1013 .join(", ");
1014
1015 let descriptions: Vec<String> = sorted_actions
1017 .iter()
1018 .map(|a| format!("- {}: {}", a.name, a.description))
1019 .collect();
1020 let descriptions_block = descriptions.join("\n");
1021
1022 let first_verb = sorted_actions
1024 .first()
1025 .map(|a| Self::extract_verb(&a.description))
1026 .unwrap_or_else(|| "CHECK".to_string());
1027
1028 format!(
1029 r#"Steps: {actions_list}
1030{descriptions_block}
1031Which step {first_verb}S first?
1032Answer:"#
1033 )
1034 }
1035
1036 pub fn generate_last_prompt(_task: &str, actions: &[ActionDef]) -> String {
1041 let mut sorted_actions: Vec<&ActionDef> = actions.iter().collect();
1043 sorted_actions.sort_by(|a, b| a.name.cmp(&b.name));
1044
1045 let actions_list = sorted_actions
1046 .iter()
1047 .map(|a| a.name.as_str())
1048 .collect::<Vec<_>>()
1049 .join(", ");
1050
1051 let descriptions: Vec<String> = sorted_actions
1052 .iter()
1053 .map(|a| format!("- {}: {}", a.name, a.description))
1054 .collect();
1055 let descriptions_block = descriptions.join("\n");
1056
1057 format!(
1058 r#"Steps: {actions_list}
1059{descriptions_block}
1060Which step should be done last?
1061Answer:"#
1062 )
1063 }
1064
1065 pub fn generate_pair_prompt(task: &str, action_a: &str, action_b: &str) -> String {
1067 format!(
1068 r#"For {task}, which comes first: {action_a} or {action_b}?
1069Answer (one word):"#
1070 )
1071 }
1072
1073 fn extract_verb(description: &str) -> String {
1078 description
1079 .split_whitespace()
1080 .next()
1081 .map(|w| {
1082 let word = w.trim_end_matches('s').trim_end_matches('S');
1084 word.to_uppercase()
1085 })
1086 .unwrap_or_else(|| "CHECK".to_string())
1087 }
1088}
1089
1090#[derive(Debug, Clone)]
1099pub struct GraphNavigator {
1100 graph: DependencyGraph,
1101 completed_actions: HashSet<String>,
1103}
1104
1105impl GraphNavigator {
1106 pub fn new(graph: DependencyGraph) -> Self {
1107 Self {
1108 graph,
1109 completed_actions: HashSet::new(),
1110 }
1111 }
1112
1113 pub fn mark_completed(&mut self, action: &str) {
1115 self.completed_actions.insert(action.to_string());
1116 }
1117
1118 pub fn suggest_next(&self) -> Vec<String> {
1124 if self.completed_actions.is_empty() {
1125 return self.graph.start_actions();
1127 }
1128
1129 let mut candidates = Vec::new();
1131 for completed in &self.completed_actions {
1132 for next in self.graph.valid_next_actions(completed) {
1133 if !self.completed_actions.contains(&next) && !candidates.contains(&next) {
1134 candidates.push(next);
1135 }
1136 }
1137 }
1138
1139 candidates
1140 }
1141
1142 pub fn is_task_complete(&self) -> bool {
1146 self.graph
1147 .terminal_actions()
1148 .iter()
1149 .any(|t| self.completed_actions.contains(t))
1150 }
1151
1152 pub fn progress(&self) -> f64 {
1154 if self.graph.available_actions.is_empty() {
1155 return 0.0;
1156 }
1157 self.completed_actions.len() as f64 / self.graph.available_actions.len() as f64
1158 }
1159
1160 pub fn graph(&self) -> &DependencyGraph {
1162 &self.graph
1163 }
1164}
1165
1166pub fn build_graph_from_action_order(
1185 task: &str,
1186 available_actions: &[String],
1187 discover: &[String],
1188 not_discover: &[String],
1189) -> Option<DependencyGraph> {
1190 if discover.is_empty() && not_discover.is_empty() {
1192 return None;
1193 }
1194
1195 let mut builder = DependencyGraphBuilder::new()
1196 .task(task)
1197 .available_actions(available_actions.iter().cloned());
1198
1199 if !discover.is_empty() {
1201 builder = builder.start_node(&discover[0]);
1202 } else if !not_discover.is_empty() {
1203 builder = builder.start_node(¬_discover[0]);
1204 }
1205
1206 if let Some(last) = not_discover.last() {
1208 builder = builder.terminal_node(last);
1209 } else if !discover.is_empty() {
1210 builder = builder.terminal_node(discover.last().unwrap());
1211 }
1212
1213 for window in discover.windows(2) {
1215 builder = builder.edge(&window[0], &window[1], 0.9);
1216 }
1217
1218 if !discover.is_empty() && !not_discover.is_empty() {
1220 builder = builder.edge(discover.last().unwrap(), ¬_discover[0], 0.9);
1221 }
1222
1223 for window in not_discover.windows(2) {
1225 builder = builder.edge(&window[0], &window[1], 0.9);
1226 }
1227
1228 builder = builder.with_orders(discover.to_vec(), not_discover.to_vec());
1230
1231 Some(builder.build())
1232}
1233
1234#[derive(Debug, Clone, Default)]
1267pub struct LearnedDependencyProvider {
1268 entries: Vec<LearnedActionOrder>,
1270 strategy: VotingStrategy,
1272}
1273
1274impl LearnedDependencyProvider {
1275 pub fn empty() -> Self {
1277 Self::default()
1278 }
1279
1280 pub fn new(action_order: LearnedActionOrder) -> Self {
1282 Self {
1283 entries: vec![action_order],
1284 strategy: VotingStrategy::default(),
1285 }
1286 }
1287
1288 pub fn with_entries(entries: Vec<LearnedActionOrder>) -> Self {
1290 Self {
1291 entries,
1292 strategy: VotingStrategy::default(),
1293 }
1294 }
1295
1296 pub fn with_strategy(mut self, strategy: VotingStrategy) -> Self {
1298 self.strategy = strategy;
1299 self
1300 }
1301
1302 pub fn add_entry(&mut self, entry: LearnedActionOrder) {
1304 self.entries.push(entry);
1305 }
1306
1307 pub fn entry_count(&self) -> usize {
1309 self.entries.len()
1310 }
1311
1312 pub fn action_order(&self) -> Option<&LearnedActionOrder> {
1314 self.entries.first()
1315 }
1316
1317 pub fn entries(&self) -> &[LearnedActionOrder] {
1319 &self.entries
1320 }
1321
1322 pub fn select(&self, task: &str, available_actions: &[String]) -> SelectResult {
1334 for entry in &self.entries {
1336 if entry.is_exact_match(available_actions) {
1337 return self.build_learned_result(task, available_actions, entry);
1338 }
1339 }
1340
1341 let mut best_match: Option<(&LearnedActionOrder, f64)> = None;
1343
1344 for entry in &self.entries {
1345 let rate = entry.match_rate(available_actions);
1346 if let Some((_, best_rate)) = best_match {
1347 if rate > best_rate {
1348 best_match = Some((entry, rate));
1349 }
1350 } else if rate > 0.0 {
1351 best_match = Some((entry, rate));
1352 }
1353 }
1354
1355 match best_match {
1357 Some((entry, rate)) if rate >= self.strategy.medium_threshold => {
1358 self.build_llm_result(entry, rate)
1359 }
1360 _ => self.build_fallback_result(),
1361 }
1362 }
1363
1364 fn build_learned_result(
1366 &self,
1367 task: &str,
1368 available_actions: &[String],
1369 entry: &LearnedActionOrder,
1370 ) -> SelectResult {
1371 let graph = build_graph_from_action_order(
1372 task,
1373 available_actions,
1374 &entry.discover,
1375 &entry.not_discover,
1376 );
1377
1378 match graph {
1379 Some(g) => {
1380 tracing::info!(
1381 discover = ?entry.discover,
1382 not_discover = ?entry.not_discover,
1383 lora = ?entry.lora.as_ref().map(|l| &l.name),
1384 "Using learned action order (LLM skipped)"
1385 );
1386 SelectResult::UseLearnedGraph {
1387 graph: Box::new(g),
1388 lora: entry.lora.clone(),
1389 }
1390 }
1391 None => {
1392 tracing::warn!("Failed to build graph from exact match, falling back to LLM");
1394 self.build_llm_result(entry, 1.0)
1395 }
1396 }
1397 }
1398
1399 fn build_llm_result(&self, entry: &LearnedActionOrder, match_rate: f64) -> SelectResult {
1401 let vote_count = self.strategy.determine(match_rate, entry.lora.is_some());
1402
1403 tracing::debug!(
1404 match_rate = match_rate,
1405 vote_count = vote_count,
1406 has_lora = entry.lora.is_some(),
1407 "LLM invocation needed (partial match)"
1408 );
1409
1410 SelectResult::UseLlm {
1411 lora: entry.lora.clone(),
1412 hint: Some(entry.clone()),
1413 vote_count,
1414 match_rate,
1415 }
1416 }
1417
1418 fn build_fallback_result(&self) -> SelectResult {
1420 tracing::debug!("No matching entry, using base model with 3 votes");
1421
1422 SelectResult::UseLlm {
1423 lora: None,
1424 hint: None,
1425 vote_count: 3,
1426 match_rate: 0.0,
1427 }
1428 }
1429}
1430
1431impl DependencyGraphProvider for LearnedDependencyProvider {
1432 fn provide_graph(&self, task: &str, available_actions: &[String]) -> Option<DependencyGraph> {
1433 match self.select(task, available_actions) {
1435 SelectResult::UseLearnedGraph { graph, .. } => {
1436 graph.validate().ok()?;
1438 Some(*graph)
1439 }
1440 SelectResult::UseLlm { .. } => {
1441 None
1443 }
1444 }
1445 }
1446
1447 fn select(&self, task: &str, available_actions: &[String]) -> Option<SelectResult> {
1448 Some(LearnedDependencyProvider::select(
1450 self,
1451 task,
1452 available_actions,
1453 ))
1454 }
1455}
1456
1457#[cfg(test)]
1462mod tests {
1463 use super::*;
1464
1465 #[test]
1466 fn test_dependency_graph_builder() {
1467 let graph = DependencyGraph::builder()
1468 .task("Find auth function")
1469 .available_actions(["Grep", "List", "Read"])
1470 .edge("Grep", "Read", 0.95)
1471 .edge("List", "Grep", 0.60)
1472 .start_nodes(["Grep", "List"])
1473 .terminal_node("Read")
1474 .build();
1475
1476 assert_eq!(graph.task(), "Find auth function");
1477 assert!(graph.is_start("Grep"));
1478 assert!(graph.is_start("List"));
1479 assert!(graph.is_terminal("Read"));
1480 assert!(graph.can_transition("Grep", "Read"));
1481 assert!(!graph.can_transition("Read", "Grep"));
1482 }
1483
1484 #[test]
1485 fn test_valid_next_actions() {
1486 let graph = DependencyGraph::builder()
1487 .available_actions(["Grep", "List", "Read"])
1488 .edge("Grep", "Read", 0.95)
1489 .edge("List", "Grep", 0.60)
1490 .edge("List", "Read", 0.40)
1491 .start_nodes(["Grep", "List"])
1492 .terminal_node("Read")
1493 .build();
1494
1495 let next = graph.valid_next_actions("Grep");
1497 assert_eq!(next, vec!["Read"]);
1498
1499 let next = graph.valid_next_actions("List");
1501 assert_eq!(next, vec!["Grep", "Read"]);
1502
1503 let next = graph.valid_next_actions("Read");
1505 assert!(next.is_empty());
1506 }
1507
1508 #[test]
1509 fn test_static_planner_file_exploration() {
1510 let planner = StaticDependencyPlanner::new().with_file_exploration_pattern();
1511
1512 let graph = planner
1513 .plan("Find auth.rs", &["Grep".to_string(), "Read".to_string()])
1514 .unwrap();
1515
1516 assert!(graph.is_start("Grep"));
1517 assert!(graph.is_terminal("Read"));
1518 }
1519
1520 #[test]
1521 fn test_graph_navigator() {
1522 let graph = DependencyGraph::builder()
1523 .available_actions(["Grep", "Read"])
1524 .edge("Grep", "Read", 0.95)
1525 .start_node("Grep")
1526 .terminal_node("Read")
1527 .build();
1528
1529 let mut nav = GraphNavigator::new(graph);
1530
1531 assert_eq!(nav.suggest_next(), vec!["Grep"]);
1533 assert!(!nav.is_task_complete());
1534
1535 nav.mark_completed("Grep");
1537 assert_eq!(nav.suggest_next(), vec!["Read"]);
1538 assert!(!nav.is_task_complete());
1539
1540 nav.mark_completed("Read");
1542 assert!(nav.is_task_complete());
1543 assert!(nav.suggest_next().is_empty());
1544 }
1545
1546 #[test]
1547 fn test_llm_response_parsing() {
1548 let json = r#"{
1549 "edges": [
1550 {"from": "Grep", "to": "Read", "confidence": 0.95}
1551 ],
1552 "start": ["Grep"],
1553 "terminal": ["Read"],
1554 "reasoning": "Search first, then read"
1555 }"#;
1556
1557 let response = LlmDependencyResponse::parse(json).unwrap();
1558 assert_eq!(response.edges.len(), 1);
1559 assert_eq!(response.start, vec!["Grep"]);
1560 assert_eq!(response.terminal, vec!["Read"]);
1561 assert!(response.reasoning.is_some());
1562
1563 let graph = response.into_graph(
1564 "Find function",
1565 vec!["Grep".to_string(), "Read".to_string()],
1566 );
1567 assert!(graph.can_transition("Grep", "Read"));
1568 }
1569
1570 #[test]
1571 fn test_mermaid_output() {
1572 let graph = DependencyGraph::builder()
1573 .available_actions(["Grep", "List", "Read"])
1574 .edge("Grep", "Read", 0.95)
1575 .edge("List", "Grep", 0.60)
1576 .start_nodes(["Grep", "List"])
1577 .terminal_node("Read")
1578 .build();
1579
1580 let mermaid = graph.to_mermaid();
1581 assert!(mermaid.contains("graph LR"));
1582 assert!(mermaid.contains("Grep -->|95%| Read"));
1583 assert!(mermaid.contains("style Read fill:#f99"));
1584 }
1585
1586 #[test]
1591 fn test_learned_action_order_hash() {
1592 let actions = vec![
1593 "Grep".to_string(),
1594 "Read".to_string(),
1595 "Restart".to_string(),
1596 ];
1597
1598 let order = LearnedActionOrder::new(
1599 vec!["Grep".to_string(), "Read".to_string()],
1600 vec!["Restart".to_string()],
1601 &actions,
1602 );
1603
1604 let actions_reordered = vec![
1606 "Restart".to_string(),
1607 "Grep".to_string(),
1608 "Read".to_string(),
1609 ];
1610 assert!(order.matches_actions(&actions_reordered));
1611
1612 let actions_different = vec!["Grep".to_string(), "Read".to_string()];
1614 assert!(!order.matches_actions(&actions_different));
1615 }
1616
1617 #[test]
1618 fn test_learned_dependency_provider_cache_hit() {
1619 let actions = vec![
1620 "Grep".to_string(),
1621 "Read".to_string(),
1622 "Restart".to_string(),
1623 ];
1624
1625 let order = LearnedActionOrder::new(
1626 vec!["Grep".to_string(), "Read".to_string()],
1627 vec!["Restart".to_string()],
1628 &actions,
1629 );
1630
1631 let provider = LearnedDependencyProvider::new(order);
1632
1633 let graph = provider.provide_graph("troubleshooting", &actions);
1635 assert!(graph.is_some());
1636
1637 let graph = graph.unwrap();
1638 assert!(graph.is_start("Grep"));
1639 assert!(graph.is_terminal("Restart"));
1640 assert!(graph.can_transition("Grep", "Read"));
1641 assert!(graph.can_transition("Read", "Restart"));
1642 }
1643
1644 #[test]
1645 fn test_learned_dependency_provider_cache_miss() {
1646 let original_actions = vec![
1647 "Grep".to_string(),
1648 "Read".to_string(),
1649 "Restart".to_string(),
1650 ];
1651
1652 let order = LearnedActionOrder::new(
1653 vec!["Grep".to_string(), "Read".to_string()],
1654 vec!["Restart".to_string()],
1655 &original_actions,
1656 );
1657
1658 let provider = LearnedDependencyProvider::new(order);
1659
1660 let different_actions = vec!["Grep".to_string(), "Read".to_string()];
1662 let graph = provider.provide_graph("troubleshooting", &different_actions);
1663 assert!(graph.is_none());
1664 }
1665
1666 #[test]
1667 fn test_learned_dependency_provider_discover_only() {
1668 let actions = vec!["Grep".to_string(), "Read".to_string()];
1669
1670 let order = LearnedActionOrder::new(
1671 vec!["Grep".to_string(), "Read".to_string()],
1672 vec![], &actions,
1674 );
1675
1676 let provider = LearnedDependencyProvider::new(order);
1677 let graph = provider.provide_graph("search task", &actions);
1678 assert!(graph.is_some());
1679
1680 let graph = graph.unwrap();
1681 assert!(graph.is_start("Grep"));
1682 assert!(graph.is_terminal("Read")); assert!(graph.can_transition("Grep", "Read"));
1684 }
1685
1686 #[test]
1687 fn test_learned_dependency_provider_not_discover_only() {
1688 let actions = vec!["Restart".to_string(), "CheckStatus".to_string()];
1689
1690 let order = LearnedActionOrder::new(
1691 vec![], vec!["Restart".to_string(), "CheckStatus".to_string()],
1693 &actions,
1694 );
1695
1696 let provider = LearnedDependencyProvider::new(order);
1697 let graph = provider.provide_graph("ops task", &actions);
1698 assert!(graph.is_some());
1699
1700 let graph = graph.unwrap();
1701 assert!(graph.is_start("Restart")); assert!(graph.is_terminal("CheckStatus"));
1703 assert!(graph.can_transition("Restart", "CheckStatus"));
1704 }
1705
1706 #[test]
1707 fn test_learned_dependency_provider_empty_lists() {
1708 let actions = vec!["Grep".to_string(), "Read".to_string()];
1709
1710 let order = LearnedActionOrder::new(
1711 vec![], vec![], &actions,
1714 );
1715
1716 let provider = LearnedDependencyProvider::new(order);
1717 let graph = provider.provide_graph("empty task", &actions);
1719 assert!(graph.is_none());
1720 }
1721
1722 #[test]
1727 fn test_extract_json_simple() {
1728 let text = r#"Here is the result: {"edges": [], "start": ["A"], "terminal": ["B"]}"#;
1729 let json = LlmDependencyResponse::extract_json(text);
1730 assert!(json.is_some());
1731 let json = json.unwrap();
1732 assert!(json.starts_with('{'));
1733 assert!(json.ends_with('}'));
1734 }
1735
1736 #[test]
1737 fn test_extract_json_nested() {
1738 let text = r#"Result: {"edges": [{"from": "A", "to": "B", "confidence": 0.9}], "start": ["A"], "terminal": ["B"]}"#;
1739 let json = LlmDependencyResponse::extract_json(text);
1740 assert!(json.is_some());
1741
1742 let parsed: Result<LlmDependencyResponse, _> = serde_json::from_str(&json.unwrap());
1744 assert!(parsed.is_ok());
1745 }
1746
1747 #[test]
1748 fn test_extract_json_with_string_braces() {
1749 let text =
1751 r#"{"edges": [], "start": ["A"], "terminal": ["B"], "reasoning": "Use {pattern}"}"#;
1752 let json = LlmDependencyResponse::extract_json(text);
1753 assert!(json.is_some());
1754 assert_eq!(json.unwrap(), text);
1755 }
1756
1757 #[test]
1758 fn test_extract_json_no_json() {
1759 let text = "This is just plain text without JSON";
1760 let json = LlmDependencyResponse::extract_json(text);
1761 assert!(json.is_none());
1762 }
1763
1764 #[test]
1769 fn test_validate_unknown_start_node() {
1770 let graph = DependencyGraph::builder()
1771 .available_actions(["Grep", "Read"])
1772 .start_node("Unknown") .terminal_node("Read")
1774 .build();
1775
1776 let result = graph.validate();
1777 assert!(result.is_err());
1778 assert!(matches!(
1779 result.unwrap_err(),
1780 DependencyGraphError::UnknownAction(name) if name == "Unknown"
1781 ));
1782 }
1783
1784 #[test]
1785 fn test_validate_unknown_terminal_node() {
1786 let graph = DependencyGraph::builder()
1787 .available_actions(["Grep", "Read"])
1788 .start_node("Grep")
1789 .terminal_node("Unknown") .build();
1791
1792 let result = graph.validate();
1793 assert!(result.is_err());
1794 assert!(matches!(
1795 result.unwrap_err(),
1796 DependencyGraphError::UnknownAction(name) if name == "Unknown"
1797 ));
1798 }
1799
1800 #[test]
1801 fn test_validate_valid_graph() {
1802 let graph = DependencyGraph::builder()
1803 .available_actions(["Grep", "Read"])
1804 .edge("Grep", "Read", 0.9)
1805 .start_node("Grep")
1806 .terminal_node("Read")
1807 .build();
1808
1809 assert!(graph.validate().is_ok());
1810 }
1811
1812 #[test]
1817 fn test_start_actions_sorted() {
1818 let graph = DependencyGraph::builder()
1819 .available_actions(["Zebra", "Apple", "Mango"])
1820 .start_nodes(["Zebra", "Apple", "Mango"])
1821 .terminal_node("Zebra")
1822 .build();
1823
1824 let actions = graph.start_actions();
1825 assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1827 }
1828
1829 #[test]
1830 fn test_terminal_actions_sorted() {
1831 let graph = DependencyGraph::builder()
1832 .available_actions(["Zebra", "Apple", "Mango"])
1833 .start_node("Apple")
1834 .terminal_nodes(["Zebra", "Apple", "Mango"])
1835 .build();
1836
1837 let actions = graph.terminal_actions();
1838 assert_eq!(actions, vec!["Apple", "Mango", "Zebra"]);
1840 }
1841
1842 #[test]
1847 fn test_voting_strategy_exact_match() {
1848 let strategy = VotingStrategy::default();
1849 assert_eq!(strategy.determine(1.0, true), 0);
1850 assert_eq!(strategy.determine(1.0, false), 0);
1851 }
1852
1853 #[test]
1854 fn test_voting_strategy_high_with_lora() {
1855 let strategy = VotingStrategy::default();
1856 assert_eq!(strategy.determine(0.85, true), 1);
1857 assert_eq!(strategy.determine(0.80, true), 1);
1858 }
1859
1860 #[test]
1861 fn test_voting_strategy_high_without_lora() {
1862 let strategy = VotingStrategy::default();
1863 assert_eq!(strategy.determine(0.85, false), 3);
1864 }
1865
1866 #[test]
1867 fn test_voting_strategy_medium() {
1868 let strategy = VotingStrategy::default();
1869 assert_eq!(strategy.determine(0.65, true), 3);
1870 assert_eq!(strategy.determine(0.60, true), 3);
1871 }
1872
1873 #[test]
1874 fn test_voting_strategy_low() {
1875 let strategy = VotingStrategy::default();
1876 assert_eq!(strategy.determine(0.5, true), 3);
1877 assert_eq!(strategy.determine(0.5, false), 3);
1878 }
1879
1880 #[test]
1885 fn test_select_exact_match_returns_learned_graph() {
1886 let actions = vec![
1887 "CheckStatus".to_string(),
1888 "ReadLogs".to_string(),
1889 "Restart".to_string(),
1890 ];
1891
1892 let order = LearnedActionOrder::new(
1893 vec!["CheckStatus".to_string(), "ReadLogs".to_string()],
1894 vec!["Restart".to_string()],
1895 &actions,
1896 );
1897
1898 let provider = LearnedDependencyProvider::new(order);
1899 let result = provider.select("test task", &actions);
1900
1901 assert!(!result.needs_llm());
1902 assert_eq!(result.vote_count(), 0);
1903 assert!(matches!(result, SelectResult::UseLearnedGraph { .. }));
1904 }
1905
1906 #[test]
1907 fn test_select_no_match_returns_llm_fallback() {
1908 let order = LearnedActionOrder::new(
1909 vec!["A".to_string(), "B".to_string()],
1910 vec!["C".to_string()],
1911 &["A".to_string(), "B".to_string(), "C".to_string()],
1912 );
1913
1914 let provider = LearnedDependencyProvider::new(order);
1915 let result = provider.select("test task", &["X".to_string(), "Y".to_string()]);
1916
1917 assert!(result.needs_llm());
1918 assert_eq!(result.vote_count(), 3);
1919 assert!(result.lora().is_none());
1920 assert!(matches!(
1921 result,
1922 SelectResult::UseLlm {
1923 hint: None,
1924 match_rate,
1925 ..
1926 } if match_rate == 0.0
1927 ));
1928 }
1929
1930 #[test]
1931 fn test_select_partial_match_with_lora_returns_1_vote() {
1932 use crate::types::LoraConfig;
1933
1934 let lora = LoraConfig {
1935 id: 1,
1936 name: Some("test-lora".to_string()),
1937 scale: 1.0,
1938 };
1939
1940 let all_actions: Vec<String> = ["A", "B", "C", "D", "E"]
1942 .iter()
1943 .map(|s| s.to_string())
1944 .collect();
1945
1946 let order = LearnedActionOrder::new(
1947 vec![
1948 "A".to_string(),
1949 "B".to_string(),
1950 "C".to_string(),
1951 "D".to_string(),
1952 ],
1953 vec!["E".to_string()],
1954 &all_actions,
1955 )
1956 .with_lora(lora);
1957
1958 let provider = LearnedDependencyProvider::new(order);
1959
1960 let query_actions: Vec<String> =
1962 ["A", "B", "C", "D"].iter().map(|s| s.to_string()).collect();
1963 let result = provider.select("test task", &query_actions);
1964
1965 assert!(result.needs_llm());
1966 assert_eq!(result.vote_count(), 1);
1968 assert!(result.lora().is_some());
1969 }
1970
1971 #[test]
1972 fn test_select_empty_provider_returns_fallback() {
1973 let provider = LearnedDependencyProvider::empty();
1974 let result = provider.select("test task", &["A".to_string()]);
1975
1976 assert!(result.needs_llm());
1977 assert_eq!(result.vote_count(), 3);
1978 assert!(result.lora().is_none());
1979 }
1980
1981 #[test]
1982 fn test_select_multiple_entries_best_match() {
1983 use crate::types::LoraConfig;
1984
1985 let order1 = LearnedActionOrder::new(
1987 vec!["A".to_string(), "B".to_string()],
1988 vec!["C".to_string()],
1989 &["A".to_string(), "B".to_string(), "C".to_string()],
1990 );
1991
1992 let lora = LoraConfig {
1994 id: 2,
1995 name: Some("better-lora".to_string()),
1996 scale: 1.0,
1997 };
1998 let order2 = LearnedActionOrder::new(
1999 vec!["A".to_string(), "B".to_string()],
2000 vec!["D".to_string()],
2001 &["A".to_string(), "B".to_string(), "D".to_string()],
2002 )
2003 .with_lora(lora);
2004
2005 let provider = LearnedDependencyProvider::with_entries(vec![order1, order2]);
2006
2007 let query = vec!["A".to_string(), "B".to_string(), "D".to_string()];
2009 let result = provider.select("test task", &query);
2010
2011 assert!(!result.needs_llm());
2012 assert!(matches!(
2013 result,
2014 SelectResult::UseLearnedGraph { lora: Some(l), .. } if l.name == Some("better-lora".to_string())
2015 ));
2016 }
2017
2018 #[test]
2019 fn test_provide_graph_exact_match_via_select() {
2020 let actions = vec![
2021 "CheckStatus".to_string(),
2022 "ReadLogs".to_string(),
2023 "Restart".to_string(),
2024 ];
2025
2026 let order = LearnedActionOrder::new(
2027 vec!["CheckStatus".to_string(), "ReadLogs".to_string()],
2028 vec!["Restart".to_string()],
2029 &actions,
2030 );
2031
2032 let provider = LearnedDependencyProvider::new(order);
2033 let graph = provider.provide_graph("test task", &actions);
2034
2035 assert!(graph.is_some());
2036 let graph = graph.unwrap();
2037 assert!(graph.is_start("CheckStatus"));
2038 assert!(graph.is_terminal("Restart"));
2039 }
2040
2041 #[test]
2042 fn test_provide_graph_no_match_returns_none() {
2043 let order = LearnedActionOrder::new(
2044 vec!["A".to_string(), "B".to_string()],
2045 vec!["C".to_string()],
2046 &["A".to_string(), "B".to_string(), "C".to_string()],
2047 );
2048
2049 let provider = LearnedDependencyProvider::new(order);
2050 let graph = provider.provide_graph("test task", &["X".to_string(), "Y".to_string()]);
2051
2052 assert!(graph.is_none());
2053 }
2054
2055 #[test]
2056 fn test_provide_graph_partial_match_returns_none() {
2057 let order = LearnedActionOrder::new(
2058 vec![
2059 "A".to_string(),
2060 "B".to_string(),
2061 "C".to_string(),
2062 "D".to_string(),
2063 ],
2064 vec!["E".to_string()],
2065 &[
2066 "A".to_string(),
2067 "B".to_string(),
2068 "C".to_string(),
2069 "D".to_string(),
2070 "E".to_string(),
2071 ],
2072 );
2073
2074 let provider = LearnedDependencyProvider::new(order);
2075
2076 let graph = provider.provide_graph(
2078 "test task",
2079 &[
2080 "A".to_string(),
2081 "B".to_string(),
2082 "C".to_string(),
2083 "D".to_string(),
2084 ],
2085 );
2086
2087 assert!(graph.is_none());
2088 }
2089}