1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use crate::documentation::{GuideTopic, EVALUATE_GUIDE};
6use crate::engine::{resolve_effective as resolve_effective_datetime, Engine};
7use crate::format_explanation;
8use crate::mcp::error::{map_engine_error, ToolError};
9use crate::parsing::ast::DateTimeValue;
10use crate::parsing::source::SourceType;
11use crate::spec_set_id::parse_spec_set_id;
12
13pub fn evaluate(engine: &Engine, args: &Value) -> Result<String, ToolError> {
14 let spec_set_id = required_string(args, "spec")?;
15 if spec_set_id.is_empty() {
16 return Err(ToolError::invalid_arguments("Spec set id cannot be empty"));
17 }
18 let spec_name = parse_spec_set_id(spec_set_id).map_err(map_engine_error)?;
19 let rule_names = optional_rule_names(args)?;
20 let data_values = parse_data(args)?;
21 let now = resolve_effective(args)?;
22 let rules = if rule_names.is_empty() {
23 None
24 } else {
25 Some(rule_names.as_slice())
26 };
27 let response = engine
28 .run(None, &spec_name, Some(&now), data_values, rules, true)
29 .map_err(map_engine_error)?;
30
31 let show_for_missing = if response
32 .results
33 .values()
34 .any(|result| result.awaits_missing_data())
35 {
36 Some(
37 engine
38 .show(None, &spec_name, Some(&now))
39 .unwrap_or_else(|error| {
40 panic!("BUG: show must succeed after evaluate for '{spec_set_id}': {error}")
41 }),
42 )
43 } else {
44 None
45 };
46
47 let mut output = String::new();
48 output.push_str(&format!("spec: {spec_set_id}\n"));
49 output.push_str(&format!("effective: {now}\n"));
50 output.push('\n');
51
52 for result in response.results.values() {
53 output.push_str(&format!("{}: ", result.rule.name));
54 if result.vetoed {
55 if let Some(reason) = result.veto_reason.as_deref() {
56 output.push_str(reason);
57 }
58 } else {
59 let display = result.display().unwrap_or_else(|| {
60 panic!(
61 "BUG: rule '{}' evaluated without display after evaluation",
62 result.rule.name
63 )
64 });
65 output.push_str(display);
66 if let Some(value) = &result.value {
67 if let Some(measure) = &value.measure {
68 append_unit_map(&mut output, measure);
69 } else if let Some(ratio) = &value.ratio {
70 append_unit_map(&mut output, ratio);
71 }
72 }
73 }
74 output.push('\n');
75
76 if result.awaits_missing_data() {
77 let show = show_for_missing
78 .as_ref()
79 .expect("BUG: any awaiting rule requires show_for_missing after evaluate");
80 output.push_str("missing_data:\n");
81 for name in result.missing_data() {
82 let entry = show.data.get(name).unwrap_or_else(|| {
83 panic!("BUG: missing_data key {name:?} must exist in show.data after evaluate")
84 });
85 let type_name = entry.lemma_type.specifications.to_string();
86 let help = entry.lemma_type.specifications.help();
87 if help.is_empty() {
88 output.push_str(&format!(" {name}: {type_name}\n"));
89 } else {
90 output.push_str(&format!(" {name}: {type_name} — {help}\n"));
91 }
92 }
93 }
94
95 if let Some(explanation) = &result.explanation {
96 let steps = format_explanation(explanation);
97 if !steps.is_empty() {
98 output.push_str("\nReasoning:\n");
99 output.push_str(&steps);
100 output.push('\n');
101 }
102 }
103 }
104
105 Ok(output)
106}
107
108pub fn list(engine: &Engine, args: &Value) -> Result<String, ToolError> {
109 require_object(args)?;
110 let list = engine.list();
111 Ok(serde_json::to_string_pretty(&list)
112 .unwrap_or_else(|error| panic!("BUG: engine list must serialize: {error}")))
113}
114
115pub fn show(engine: &Engine, args: &Value) -> Result<String, ToolError> {
116 let spec_set_id = required_string(args, "spec")?;
117 if spec_set_id.is_empty() {
118 return Err(ToolError::invalid_arguments("Spec set id cannot be empty"));
119 }
120 let spec_name = parse_spec_set_id(spec_set_id).map_err(map_engine_error)?;
121 let now = resolve_effective(args)?;
122 let show = engine
123 .show(None, &spec_name, Some(&now))
124 .map_err(map_engine_error)?;
125 Ok(serde_json::to_string_pretty(&show)
126 .unwrap_or_else(|error| panic!("BUG: show response must serialize: {error}")))
127}
128
129pub fn source(engine: &Engine, args: &Value) -> Result<String, ToolError> {
130 require_object(args)?;
131 if let Some(repository) = optional_nonempty_string(args, "repository")? {
132 return engine
133 .source(Some(repository), None, None)
134 .map_err(map_engine_error);
135 }
136 let spec_set_id = required_string(args, "spec")
137 .map_err(|_| ToolError::invalid_arguments("Missing 'spec' or 'repository' field"))?;
138 let spec_name = parse_spec_set_id(spec_set_id).map_err(map_engine_error)?;
139 let now = resolve_effective(args)?;
140 engine
141 .source(None, Some(&spec_name), Some(&now))
142 .map_err(map_engine_error)
143}
144
145pub fn check(args: &Value) -> Result<String, ToolError> {
146 let sources_value = args
147 .get("sources")
148 .ok_or_else(|| ToolError::invalid_arguments("Missing 'sources' array field"))?;
149 let sources_arr = sources_value
150 .as_array()
151 .ok_or_else(|| ToolError::invalid_arguments("Missing 'sources' array field"))?;
152 if sources_arr.is_empty() {
153 return Err(ToolError::invalid_arguments(
154 "'sources' must be a non-empty array of [label, code] pairs",
155 ));
156 }
157
158 let mut sources: Vec<(SourceType, String)> = Vec::with_capacity(sources_arr.len());
159 for (i, entry) in sources_arr.iter().enumerate() {
160 let pair = entry.as_array().ok_or_else(|| {
161 ToolError::invalid_arguments(format!("sources[{i}] must be a [label, code] array"))
162 })?;
163 if pair.len() != 2 {
164 return Err(ToolError::invalid_arguments(format!(
165 "sources[{i}] must have exactly 2 elements [label, code]"
166 )));
167 }
168 let label = pair[0].as_str().ok_or_else(|| {
169 ToolError::invalid_arguments(format!("sources[{i}][0] (label) must be a string"))
170 })?;
171 let code = pair[1].as_str().ok_or_else(|| {
172 ToolError::invalid_arguments(format!("sources[{i}][1] (code) must be a string"))
173 })?;
174 let source_type =
175 SourceType::from_binding_label(label).map_err(ToolError::invalid_arguments)?;
176 sources.push((source_type, code.to_string()));
177 }
178
179 let mut engine = Engine::new();
180 if let Err(load_err) = engine.load(sources) {
181 return Err(ToolError::diagnostics(&load_err.errors));
182 }
183
184 let recommendations = engine.quality();
185 let mut text = String::from(
186 "Parsed and planned. Syntax is valid; this does not verify the policy is correct.",
187 );
188 const MAX_RECOMMENDATIONS: usize = 20;
189 if !recommendations.is_empty() {
190 text.push_str("\n\nRecommendations:");
191 for (i, rec) in recommendations.iter().take(MAX_RECOMMENDATIONS).enumerate() {
192 text.push_str(&format!("\n{}. {}", i + 1, rec));
193 }
194 let omitted = recommendations.len().saturating_sub(MAX_RECOMMENDATIONS);
195 if omitted > 0 {
196 text.push_str(&format!("\n… and {omitted} more"));
197 }
198 }
199 Ok(text)
200}
201
202pub fn guide(args: &Value) -> Result<String, ToolError> {
203 require_object(args)?;
204 match args.get("topic") {
205 None => Ok(EVALUATE_GUIDE.to_string()),
206 Some(value) => {
207 let topic_name = value
208 .as_str()
209 .ok_or_else(|| ToolError::invalid_arguments("topic must be a string"))?;
210 let topic = GuideTopic::parse(topic_name).ok_or_else(|| {
211 ToolError::invalid_arguments(format!(
212 "Unknown guide topic '{topic_name}'. Valid: {}",
213 GuideTopic::VALID_LIST
214 ))
215 })?;
216 Ok(topic.section_text().to_string())
217 }
218 }
219}
220
221fn require_object(args: &Value) -> Result<(), ToolError> {
222 if args.is_object() || args.is_null() {
223 Ok(())
224 } else {
225 Err(ToolError::invalid_arguments("arguments must be an object"))
226 }
227}
228
229fn required_string<'a>(args: &'a Value, field: &str) -> Result<&'a str, ToolError> {
230 match args.get(field) {
231 Some(Value::String(value)) => Ok(value.trim()),
232 Some(_) => Err(ToolError::invalid_arguments(format!(
233 "'{field}' must be a string"
234 ))),
235 None => Err(ToolError::invalid_arguments(format!(
236 "Missing '{field}' field"
237 ))),
238 }
239}
240
241fn optional_nonempty_string<'a>(
242 args: &'a Value,
243 field: &str,
244) -> Result<Option<&'a str>, ToolError> {
245 match args.get(field) {
246 None => Ok(None),
247 Some(Value::Null) => Ok(None),
248 Some(Value::String(value)) => {
249 let trimmed = value.trim();
250 if trimmed.is_empty() {
251 Ok(None)
252 } else {
253 Ok(Some(trimmed))
254 }
255 }
256 Some(_) => Err(ToolError::invalid_arguments(format!(
257 "'{field}' must be a string"
258 ))),
259 }
260}
261
262fn optional_rule_names(args: &Value) -> Result<Vec<String>, ToolError> {
263 match args.get("rule") {
264 None | Some(Value::Null) => Ok(Vec::new()),
265 Some(Value::String(rule)) => {
266 let trimmed = rule.trim();
267 if trimmed.is_empty() {
268 Ok(Vec::new())
269 } else {
270 Ok(vec![trimmed.to_string()])
271 }
272 }
273 Some(_) => Err(ToolError::invalid_arguments("'rule' must be a string")),
274 }
275}
276
277fn parse_data(args: &Value) -> Result<HashMap<String, String>, ToolError> {
278 match args.get("data") {
279 None | Some(Value::Null) => Ok(HashMap::new()),
280 Some(Value::Array(entries)) => {
281 let mut data = HashMap::new();
282 for (i, entry) in entries.iter().enumerate() {
283 let Some(raw) = entry.as_str() else {
284 return Err(ToolError::invalid_arguments(format!(
285 "data[{i}] must be a string 'name=value'"
286 )));
287 };
288 let Some((name, value)) = raw.split_once('=') else {
289 return Err(ToolError::invalid_arguments(format!(
290 "data[{i}] must be 'name=value', got '{raw}'"
291 )));
292 };
293 data.insert(name.to_string(), value.to_string());
294 }
295 Ok(data)
296 }
297 Some(_) => Err(ToolError::invalid_arguments(
298 "'data' must be an array of 'name=value' strings",
299 )),
300 }
301}
302
303fn resolve_effective(args: &Value) -> Result<DateTimeValue, ToolError> {
304 match args.get("effective") {
305 None | Some(Value::Null) => resolve_effective_datetime(None).map_err(map_engine_error),
306 Some(Value::String(raw)) => resolve_effective_datetime(Some(raw)).map_err(map_engine_error),
307 Some(_) => Err(ToolError::invalid_arguments("'effective' must be a string")),
308 }
309}
310
311fn append_unit_map(output: &mut String, map: &std::collections::BTreeMap<String, String>) {
312 let parts: Vec<String> = map
313 .iter()
314 .map(|(unit, magnitude)| format!("{unit} {magnitude}"))
315 .collect();
316 output.push_str(" (");
317 output.push_str(&parts.join(", "));
318 output.push(')');
319}