forge_guardrails/core/
steps.rs1use indexmap::{IndexMap, IndexSet};
2use serde_json::Value;
3
4#[derive(Debug, Clone, PartialEq)]
6pub struct PrerequisiteCheck {
7 pub satisfied: bool,
9 pub missing: Vec<String>,
11}
12
13impl PrerequisiteCheck {
14 pub fn satisfied() -> Self {
16 Self {
17 satisfied: true,
18 missing: Vec::new(),
19 }
20 }
21
22 pub fn unsatisfied(missing: Vec<String>) -> Self {
24 Self {
25 satisfied: false,
26 missing,
27 }
28 }
29}
30
31#[derive(Debug, Clone, PartialEq)]
33pub enum Prerequisite {
34 NameOnly(String),
36 ArgMatched {
38 tool: String,
40 match_arg: String,
42 },
43}
44
45pub struct StepTracker {
47 required_steps: Vec<String>,
48 completed_steps: IndexSet<String>,
49 executed_tools: IndexMap<String, Vec<IndexMap<String, Value>>>,
50}
51
52impl StepTracker {
53 pub fn new(required_steps: Vec<String>) -> Self {
55 Self {
56 required_steps,
57 completed_steps: IndexSet::new(),
58 executed_tools: IndexMap::new(),
59 }
60 }
61
62 pub fn record(&mut self, tool_name: &str, args: Option<&IndexMap<String, Value>>) {
67 self.completed_steps.insert(tool_name.to_string());
68 let empty = IndexMap::new();
69 let args = args.unwrap_or(&empty);
70 self.executed_tools
71 .entry(tool_name.to_string())
72 .or_default()
73 .push(args.clone());
74 }
75
76 pub fn is_satisfied(&self) -> bool {
79 self.required_steps
80 .iter()
81 .all(|step| self.completed_steps.contains(step))
82 }
83
84 pub fn pending(&self) -> Vec<String> {
86 self.required_steps
87 .iter()
88 .filter(|step| !self.completed_steps.contains(step.as_str()))
89 .cloned()
90 .collect()
91 }
92
93 pub fn check_prerequisites(
95 &self,
96 _tool_name: &str,
97 args: &IndexMap<String, Value>,
98 prerequisites: &[Prerequisite],
99 ) -> PrerequisiteCheck {
100 let mut missing = Vec::new();
101
102 for prereq in prerequisites {
103 match prereq {
104 Prerequisite::NameOnly(tool) => {
105 if !self.completed_steps.contains(tool.as_str()) {
106 missing.push(tool.clone());
107 }
108 }
109 Prerequisite::ArgMatched { tool, match_arg } => {
110 let current_val = args.get(match_arg).cloned().unwrap_or(Value::Null);
111 let found = self
112 .executed_tools
113 .get(tool.as_str())
114 .map(|invocations| {
115 invocations.iter().any(|inv| {
116 inv.get(match_arg).cloned().unwrap_or(Value::Null) == current_val
117 })
118 })
119 .unwrap_or(false);
120 if !found {
121 missing.push(tool.clone());
122 }
123 }
124 }
125 }
126
127 if missing.is_empty() {
128 PrerequisiteCheck::satisfied()
129 } else {
130 PrerequisiteCheck::unsatisfied(missing)
131 }
132 }
133
134 pub fn summary_hint(&self) -> String {
139 if self.completed_steps.is_empty() {
140 "[No steps completed yet]".to_string()
141 } else {
142 let names: Vec<&str> = self.completed_steps.iter().map(|s| s.as_str()).collect();
143 format!("[Steps completed: {}]", names.join(", "))
144 }
145 }
146
147 pub fn completed_count(&self) -> usize {
149 self.completed_steps.len()
150 }
151
152 pub fn required_steps(&self) -> &[String] {
154 &self.required_steps
155 }
156}