datasynth_audit_optimizer/
constrained.rs1use std::collections::{HashMap, HashSet};
4
5use serde::Serialize;
6
7use datasynth_audit_fsm::schema::AuditBlueprint;
8
9use crate::shortest_path::{analyze_shortest_paths, ProcedurePath, ShortestPathReport};
10
11#[derive(Debug, Clone, Serialize)]
17pub struct ConstrainedPathResult {
18 pub required_procedures: Vec<String>,
21 pub total_transitions: usize,
23 pub paths: ShortestPathReport,
25}
26
27pub fn constrained_path(
43 blueprint: &AuditBlueprint,
44 must_visit: &[String],
45 preconditions: &HashMap<String, Vec<String>>,
46) -> ConstrainedPathResult {
47 let mut required: HashSet<String> = must_visit.iter().cloned().collect();
51 let mut queue: Vec<String> = must_visit.to_vec();
52
53 while let Some(proc_id) = queue.pop() {
54 if let Some(deps) = preconditions.get(&proc_id) {
55 for dep in deps {
56 if required.insert(dep.clone()) {
57 queue.push(dep.clone());
58 }
59 }
60 }
61 }
62
63 let full_paths = analyze_shortest_paths(blueprint);
67 let filtered: HashMap<String, ProcedurePath> = full_paths
68 .procedure_paths
69 .into_iter()
70 .filter(|(k, _)| required.contains(k))
71 .collect();
72
73 let total = filtered.values().map(|p| p.transition_count).sum();
74
75 let mut required_sorted: Vec<String> = required.into_iter().collect();
76 required_sorted.sort();
77
78 ConstrainedPathResult {
79 required_procedures: required_sorted,
80 total_transitions: total,
81 paths: ShortestPathReport {
82 procedure_paths: filtered,
83 total_minimum_transitions: total,
84 },
85 }
86}
87
88#[cfg(test)]
93mod tests {
94 use super::*;
95 use datasynth_audit_fsm::loader::BlueprintWithPreconditions;
96
97 fn load_fsa() -> BlueprintWithPreconditions {
98 BlueprintWithPreconditions::load_builtin_fsa().expect("builtin FSA blueprint should load")
99 }
100
101 #[test]
104 fn test_constrained_path_expands_preconditions() {
105 let bwp = load_fsa();
106 let must_visit = vec!["form_opinion".to_string()];
107
108 let result = constrained_path(&bwp.blueprint, &must_visit, &bwp.preconditions);
109
110 assert!(
111 result
112 .required_procedures
113 .contains(&"form_opinion".to_string()),
114 "form_opinion must be in required_procedures"
115 );
116 assert!(
117 result
118 .required_procedures
119 .contains(&"going_concern".to_string()),
120 "going_concern must be transitively included"
121 );
122 assert!(
123 result
124 .required_procedures
125 .contains(&"subsequent_events".to_string()),
126 "subsequent_events must be transitively included"
127 );
128 assert!(
129 result.required_procedures.len() >= 3,
130 "expected at least 3 required procedures, got {}",
131 result.required_procedures.len()
132 );
133 }
134
135 #[test]
136 fn test_constrained_path_paths_are_filtered() {
137 let bwp = load_fsa();
138 let must_visit = vec!["form_opinion".to_string()];
139
140 let result = constrained_path(&bwp.blueprint, &must_visit, &bwp.preconditions);
141
142 for key in result.paths.procedure_paths.keys() {
144 assert!(
145 result.required_procedures.contains(key),
146 "path key '{}' is not in required_procedures",
147 key
148 );
149 }
150 }
151
152 #[test]
153 fn test_constrained_path_total_transitions_consistent() {
154 let bwp = load_fsa();
155 let must_visit = vec!["form_opinion".to_string()];
156
157 let result = constrained_path(&bwp.blueprint, &must_visit, &bwp.preconditions);
158
159 let expected_total: usize = result
160 .paths
161 .procedure_paths
162 .values()
163 .map(|p| p.transition_count)
164 .sum();
165 assert_eq!(
166 result.total_transitions, expected_total,
167 "total_transitions should equal sum of per-procedure transition counts"
168 );
169 assert_eq!(
170 result.paths.total_minimum_transitions, expected_total,
171 "ShortestPathReport.total_minimum_transitions should match"
172 );
173 }
174
175 #[test]
176 fn test_constrained_path_empty_must_visit() {
177 let bwp = load_fsa();
178 let result = constrained_path(&bwp.blueprint, &[], &bwp.preconditions);
179
180 assert!(
181 result.required_procedures.is_empty(),
182 "empty must_visit should produce empty required_procedures"
183 );
184 assert_eq!(result.total_transitions, 0);
185 }
186
187 #[test]
188 fn test_constrained_path_serializes() {
189 let bwp = load_fsa();
190 let must_visit = vec!["form_opinion".to_string()];
191 let result = constrained_path(&bwp.blueprint, &must_visit, &bwp.preconditions);
192
193 let json = serde_json::to_string(&result).expect("should serialize to JSON");
194 assert!(json.contains("required_procedures"));
195 assert!(json.contains("total_transitions"));
196 }
197}