1use std::sync::Arc;
11
12use sim_kernel::{Cx, DefaultFactory, Expr, 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(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
206 let matched = crate::shapes::intent_shape()
207 .check_expr(&mut cx, expr)
208 .map_err(|error| IntentError::at(&[], format!("Intent shape check failed: {error}")))?;
209 Ok(
210 (!matched.accepted)
211 .then(|| rejection_message(&matched, "value is not a recognized Intent")),
212 )
213}
214
215fn rejection_message(matched: &ShapeMatch, fallback: &str) -> String {
216 matched
217 .diagnostics
218 .first()
219 .map(|diagnostic| diagnostic.message.clone())
220 .unwrap_or_else(|| fallback.to_owned())
221}
222
223fn validate_origin(entries: &[(Expr, Expr)]) -> Result<(), IntentError> {
224 let Some(origin) = access::entry_field(entries, ORIGIN_KEY) else {
225 return Err(IntentError::at(
226 &[ORIGIN_KEY],
227 "Intent is missing an 'origin'",
228 ));
229 };
230 let Expr::Map(origin_entries) = origin else {
231 return Err(IntentError::at(
232 &[ORIGIN_KEY],
233 "Intent 'origin' must be a map",
234 ));
235 };
236 match access::entry_field(origin_entries, OPERATOR_KEY) {
237 Some(Expr::Symbol(symbol)) if Operator::from_name(&symbol.name).is_some() => {}
238 _ => {
239 return Err(IntentError::at(
240 &[ORIGIN_KEY, OPERATOR_KEY],
241 "origin 'operator' must be 'human' or 'agent'",
242 ));
243 }
244 }
245 match access::entry_field(origin_entries, AT_TICK_KEY) {
246 Some(Expr::Number(_)) => Ok(()),
247 _ => Err(IntentError::at(
248 &[ORIGIN_KEY, AT_TICK_KEY],
249 "origin 'at-tick' must be a number",
250 )),
251 }
252}
253
254pub fn resolve_targets(expr: &Expr, is_known: impl Fn(&Expr) -> bool) -> Result<(), IntentError> {
258 for (label, target) in referenced_targets(expr) {
259 if !is_known(&target) {
260 return Err(IntentError {
261 path: vec![label],
262 message: "Intent references an unknown target".to_owned(),
263 });
264 }
265 }
266 Ok(())
267}
268
269pub fn referenced_targets(expr: &Expr) -> Vec<(String, Expr)> {
272 let Some(kind) = intent_kind_of(expr) else {
273 return Vec::new();
274 };
275 let mut refs = Vec::new();
276 let mut single = |name: &str| {
277 if let Some(value) = field(expr, name) {
278 refs.push((name.to_owned(), value.clone()));
279 }
280 };
281 match &*kind.name {
282 "tap" | "edit" | "edit-field" | "invoke" | "scrub" | "set-param" | "performance-event"
283 | "piano-roll-edit" | "player-rack-edit" | "arranger-edit" => single("target"),
284 "move" => single("node"),
285 "unwire" => single("edge"),
286 "create" => single("class"),
287 "open" => single("value"),
288 "approve" | "reject" | "ask" | "split-mission" | "pause-agent" | "rerun-validation"
289 | "replay-cassette" => single("mission"),
290 "open-source" => single("location"),
291 "select" | "delete" => {
292 if let Some(Expr::List(items)) = field(expr, "targets") {
293 for (index, item) in items.iter().enumerate() {
294 refs.push((format!("targets[{index}]"), item.clone()));
295 }
296 }
297 }
298 "wire" => {
299 for end in ["from", "to"] {
300 if let Some(Expr::Map(port)) = field(expr, end)
301 && let Some(node) = access::entry_field(port, "node")
302 {
303 refs.push((format!("{end}.node"), node.clone()));
304 }
305 }
306 }
307 _ => {}
308 }
309 refs
310}