1use std::sync::Arc;
11
12use sim_kernel::{Cx, DefaultFactory, Expr, HandleSeed, NoopEvalPolicy, ShapeMatch, Symbol};
13use sim_value::access;
14
15use crate::kinds::{
16 AT_TICK_KEY, KIND_KEY, OPERATOR_KEY, ORIGIN_KEY, is_known_kind, required_fields,
17};
18
19pub use sim_value::access::field;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Operator {
25 Human,
27 Agent,
29}
30
31impl Operator {
32 pub fn symbol(self) -> Symbol {
34 match self {
35 Operator::Human => Symbol::new("human"),
36 Operator::Agent => Symbol::new("agent"),
37 }
38 }
39
40 pub fn from_name(name: &str) -> Option<Self> {
42 match name {
43 "human" => Some(Operator::Human),
44 "agent" => Some(Operator::Agent),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct Origin {
53 pub operator: Operator,
55 pub at_tick: u64,
57}
58
59impl Origin {
60 pub fn human(tick: u64) -> Self {
62 Self {
63 operator: Operator::Human,
64 at_tick: tick,
65 }
66 }
67
68 pub fn agent(tick: u64) -> Self {
70 Self {
71 operator: Operator::Agent,
72 at_tick: tick,
73 }
74 }
75
76 fn to_expr(self) -> Expr {
77 sim_value::build::map(vec![
78 (OPERATOR_KEY, Expr::Symbol(self.operator.symbol())),
79 (AT_TICK_KEY, sim_value::build::uint(self.at_tick)),
80 ])
81 }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct IntentError {
88 pub path: Vec<String>,
90 pub message: String,
92}
93
94impl IntentError {
95 fn at(path: &[&str], message: impl Into<String>) -> Self {
96 Self {
97 path: path.iter().map(|segment| (*segment).to_owned()).collect(),
98 message: message.into(),
99 }
100 }
101
102 pub fn path_string(&self) -> String {
104 if self.path.is_empty() {
105 "<root>".to_owned()
106 } else {
107 self.path.join(".")
108 }
109 }
110}
111
112impl core::fmt::Display for IntentError {
113 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
114 write!(f, "{}: {}", self.path_string(), self.message)
115 }
116}
117
118pub fn intent(kind_name: &str, origin: Origin, fields: Vec<(&str, Expr)>) -> Expr {
120 let mut pairs = Vec::with_capacity(fields.len() + 2);
121 pairs.push((
122 sim_value::build::sym(KIND_KEY),
123 Expr::Symbol(crate::kinds::intent_kind(kind_name)),
124 ));
125 pairs.push((sim_value::build::sym(ORIGIN_KEY), origin.to_expr()));
126 for (key, value) in fields {
127 pairs.push((sim_value::build::sym(key), value));
128 }
129 Expr::Map(pairs)
130}
131
132pub fn intent_kind_of(expr: &Expr) -> Option<Symbol> {
134 let Expr::Map(entries) = expr else {
135 return None;
136 };
137 match access::entry_field(entries, KIND_KEY) {
138 Some(Expr::Symbol(kind)) => Some(kind.clone()),
139 _ => None,
140 }
141}
142
143pub fn origin(expr: &Expr) -> Option<Origin> {
145 let origin = field(expr, ORIGIN_KEY)?;
146 let Expr::Map(entries) = origin else {
147 return None;
148 };
149 let operator = match access::entry_field(entries, OPERATOR_KEY) {
150 Some(Expr::Symbol(symbol)) => Operator::from_name(&symbol.name)?,
151 _ => return None,
152 };
153 let at_tick = match access::entry_field(entries, AT_TICK_KEY) {
154 Some(Expr::Number(number)) => number.canonical.parse::<u64>().ok()?,
155 _ => return None,
156 };
157 Some(Origin { operator, at_tick })
158}
159
160pub fn validate_intent(expr: &Expr) -> Result<(), IntentError> {
163 let shape_error = check_intent_shape(expr)?;
164 let Expr::Map(entries) = expr else {
165 return Err(IntentError::at(&[], "an Intent must be a map"));
166 };
167 let kind = match access::entry_field(entries, KIND_KEY) {
168 Some(Expr::Symbol(kind)) if is_known_kind(kind) => kind.clone(),
169 Some(Expr::Symbol(kind)) => {
170 return Err(IntentError::at(
171 &[KIND_KEY],
172 format!("unrecognized Intent kind '{kind}'"),
173 ));
174 }
175 Some(_) => {
176 return Err(IntentError::at(
177 &[KIND_KEY],
178 "Intent 'kind' must be a symbol",
179 ));
180 }
181 None => return Err(IntentError::at(&[], "Intent is missing a 'kind' tag")),
182 };
183 if let Some(message) = shape_error {
184 return Err(IntentError::at(&[], message));
185 }
186 validate_origin(entries)?;
187 for required in required_fields(&kind.name) {
188 let Some(value) = access::entry_field(entries, required) else {
189 return Err(IntentError::at(
190 &[required],
191 format!("Intent '{kind}' is missing required field '{required}'"),
192 ));
193 };
194 if *required == "path" && !matches!(value, Expr::List(_)) {
195 return Err(IntentError::at(
196 &["path"],
197 format!("{kind} 'path' must be a list of segments"),
198 ));
199 }
200 }
201 Ok(())
202}
203
204fn check_intent_shape(expr: &Expr) -> Result<Option<String>, IntentError> {
205 let mut cx = Cx::new(
206 Arc::new(NoopEvalPolicy),
207 Arc::new(DefaultFactory),
208 HandleSeed::new(1),
209 );
210 let matched = crate::shapes::intent_shape()
211 .check_expr(&mut cx, expr)
212 .map_err(|error| IntentError::at(&[], format!("Intent shape check failed: {error}")))?;
213 Ok(
214 (!matched.accepted)
215 .then(|| rejection_message(&matched, "value is not a recognized Intent")),
216 )
217}
218
219fn rejection_message(matched: &ShapeMatch, fallback: &str) -> String {
220 matched
221 .diagnostics
222 .first()
223 .map(|diagnostic| diagnostic.message.clone())
224 .unwrap_or_else(|| fallback.to_owned())
225}
226
227fn validate_origin(entries: &[(Expr, Expr)]) -> Result<(), IntentError> {
228 let Some(origin) = access::entry_field(entries, ORIGIN_KEY) else {
229 return Err(IntentError::at(
230 &[ORIGIN_KEY],
231 "Intent is missing an 'origin'",
232 ));
233 };
234 let Expr::Map(origin_entries) = origin else {
235 return Err(IntentError::at(
236 &[ORIGIN_KEY],
237 "Intent 'origin' must be a map",
238 ));
239 };
240 match access::entry_field(origin_entries, OPERATOR_KEY) {
241 Some(Expr::Symbol(symbol)) if Operator::from_name(&symbol.name).is_some() => {}
242 _ => {
243 return Err(IntentError::at(
244 &[ORIGIN_KEY, OPERATOR_KEY],
245 "origin 'operator' must be 'human' or 'agent'",
246 ));
247 }
248 }
249 match access::entry_field(origin_entries, AT_TICK_KEY) {
250 Some(Expr::Number(_)) => Ok(()),
251 _ => Err(IntentError::at(
252 &[ORIGIN_KEY, AT_TICK_KEY],
253 "origin 'at-tick' must be a number",
254 )),
255 }
256}
257
258pub fn resolve_targets(expr: &Expr, is_known: impl Fn(&Expr) -> bool) -> Result<(), IntentError> {
262 for (label, target) in referenced_targets(expr) {
263 if !is_known(&target) {
264 return Err(IntentError {
265 path: vec![label],
266 message: "Intent references an unknown target".to_owned(),
267 });
268 }
269 }
270 Ok(())
271}
272
273pub fn referenced_targets(expr: &Expr) -> Vec<(String, Expr)> {
276 let Some(kind) = intent_kind_of(expr) else {
277 return Vec::new();
278 };
279 let mut refs = Vec::new();
280 let mut single = |name: &str| {
281 if let Some(value) = field(expr, name) {
282 refs.push((name.to_owned(), value.clone()));
283 }
284 };
285 match &*kind.name {
286 "tap" | "edit" | "edit-field" | "invoke" | "scrub" | "set-param" | "performance-event"
287 | "piano-roll-edit" | "player-rack-edit" | "arranger-edit" => single("target"),
288 "move" => single("node"),
289 "unwire" => single("edge"),
290 "create" => single("class"),
291 "open" => single("value"),
292 "approve" | "reject" | "ask" | "split-mission" | "pause-agent" | "rerun-validation"
293 | "replay-cassette" => single("mission"),
294 "open-source" => single("location"),
295 "select" | "delete" => {
296 if let Some(Expr::List(items)) = field(expr, "targets") {
297 for (index, item) in items.iter().enumerate() {
298 refs.push((format!("targets[{index}]"), item.clone()));
299 }
300 }
301 }
302 "wire" => {
303 for end in ["from", "to"] {
304 if let Some(Expr::Map(port)) = field(expr, end)
305 && let Some(node) = access::entry_field(port, "node")
306 {
307 refs.push((format!("{end}.node"), node.clone()));
308 }
309 }
310 }
311 _ => {}
312 }
313 refs
314}