Skip to main content

sim_lib_intent/
model.rs

1//! Intent value model: origin, builders, accessors, and fail-closed validation.
2//!
3//! An Intent is a SIM value: a `kind`-tagged `Expr::Map` that also carries an
4//! `origin` (operator plus logical tick) and the fields its kind requires. This
5//! module builds and inspects Intents over `Expr` (no parallel data model),
6//! validates them structurally into a [`IntentError`], and resolves the targets
7//! an Intent references against a caller-supplied predicate so an Intent naming
8//! an unknown target produces a diagnostic rather than a partial mutation.
9
10use sim_kernel::{Expr, Symbol};
11
12use crate::kinds::{
13    AT_TICK_KEY, KIND_KEY, OPERATOR_KEY, ORIGIN_KEY, is_known_kind, required_fields,
14};
15
16/// Who issued an Intent. Recorded on every Intent for audit; both a human
17/// (through the browser) and an agent (through the runner) are peers on the bus.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Operator {
20    /// A human operator gesturing through the browser shell.
21    Human,
22    /// An agent operator acting through the agent runner.
23    Agent,
24}
25
26impl Operator {
27    /// The operator symbol used inside an Intent origin.
28    pub fn symbol(self) -> Symbol {
29        match self {
30            Operator::Human => Symbol::new("human"),
31            Operator::Agent => Symbol::new("agent"),
32        }
33    }
34
35    /// Parse an operator symbol, or `None` if it names neither operator.
36    pub fn from_name(name: &str) -> Option<Self> {
37        match name {
38            "human" => Some(Operator::Human),
39            "agent" => Some(Operator::Agent),
40            _ => None,
41        }
42    }
43}
44
45/// The origin of an Intent: which operator issued it and at what logical tick.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct Origin {
48    /// The issuing operator.
49    pub operator: Operator,
50    /// A monotonically increasing logical tick.
51    pub at_tick: u64,
52}
53
54impl Origin {
55    /// Build an origin for a human operator at `tick`.
56    pub fn human(tick: u64) -> Self {
57        Self {
58            operator: Operator::Human,
59            at_tick: tick,
60        }
61    }
62
63    /// Build an origin for an agent operator at `tick`.
64    pub fn agent(tick: u64) -> Self {
65        Self {
66            operator: Operator::Agent,
67            at_tick: tick,
68        }
69    }
70
71    fn to_expr(self) -> Expr {
72        sim_value::build::map(vec![
73            (OPERATOR_KEY, Expr::Symbol(self.operator.symbol())),
74            (AT_TICK_KEY, sim_value::build::uint(self.at_tick)),
75        ])
76    }
77}
78
79/// A structured Intent validation diagnostic: where the problem is and what it
80/// is.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct IntentError {
83    /// Address into the Intent (for example `path` or `targets[1]`).
84    pub path: Vec<String>,
85    /// Human-readable description of the violation.
86    pub message: String,
87}
88
89impl IntentError {
90    fn at(path: &[&str], message: impl Into<String>) -> Self {
91        Self {
92            path: path.iter().map(|segment| (*segment).to_owned()).collect(),
93            message: message.into(),
94        }
95    }
96
97    /// Render the path as a dotted address, or `<root>` when empty.
98    pub fn path_string(&self) -> String {
99        if self.path.is_empty() {
100            "<root>".to_owned()
101        } else {
102            self.path.join(".")
103        }
104    }
105}
106
107impl core::fmt::Display for IntentError {
108    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109        write!(f, "{}: {}", self.path_string(), self.message)
110    }
111}
112
113/// Build an Intent value: a `kind`-tagged map carrying `origin` then `fields`.
114pub fn intent(kind_name: &str, origin: Origin, fields: Vec<(&str, Expr)>) -> Expr {
115    let mut pairs = Vec::with_capacity(fields.len() + 2);
116    pairs.push((
117        sim_value::build::sym(KIND_KEY),
118        Expr::Symbol(crate::kinds::intent_kind(kind_name)),
119    ));
120    pairs.push((sim_value::build::sym(ORIGIN_KEY), origin.to_expr()));
121    for (key, value) in fields {
122        pairs.push((sim_value::build::sym(key), value));
123    }
124    Expr::Map(pairs)
125}
126
127fn entry<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
128    entries.iter().find_map(|(key, value)| {
129        matches!(key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
130            .then_some(value)
131    })
132}
133
134/// If `expr` is a `kind`-tagged map, return the kind symbol.
135pub fn intent_kind_of(expr: &Expr) -> Option<Symbol> {
136    let Expr::Map(entries) = expr else {
137        return None;
138    };
139    match entry(entries, KIND_KEY) {
140        Some(Expr::Symbol(kind)) => Some(kind.clone()),
141        _ => None,
142    }
143}
144
145/// Read a top-level Intent field by name.
146pub fn field<'a>(expr: &'a Expr, name: &str) -> Option<&'a Expr> {
147    sim_value::access::field(expr, name)
148}
149
150/// Parse the origin of an Intent, if present and well-formed.
151pub fn origin(expr: &Expr) -> Option<Origin> {
152    let origin = field(expr, ORIGIN_KEY)?;
153    let Expr::Map(entries) = origin else {
154        return None;
155    };
156    let operator = match entry(entries, OPERATOR_KEY) {
157        Some(Expr::Symbol(symbol)) => Operator::from_name(&symbol.name)?,
158        _ => return None,
159    };
160    let at_tick = match entry(entries, AT_TICK_KEY) {
161        Some(Expr::Number(number)) => number.canonical.parse::<u64>().ok()?,
162        _ => return None,
163    };
164    Some(Origin { operator, at_tick })
165}
166
167/// Validate that `expr` is a structurally well-formed Intent, failing closed
168/// with an [`IntentError`] otherwise.
169pub fn validate_intent(expr: &Expr) -> Result<(), IntentError> {
170    let Expr::Map(entries) = expr else {
171        return Err(IntentError::at(&[], "an Intent must be a map"));
172    };
173    let kind = match entry(entries, KIND_KEY) {
174        Some(Expr::Symbol(kind)) if is_known_kind(kind) => kind.clone(),
175        Some(Expr::Symbol(kind)) => {
176            return Err(IntentError::at(
177                &[KIND_KEY],
178                format!("unrecognized Intent kind '{kind}'"),
179            ));
180        }
181        Some(_) => {
182            return Err(IntentError::at(
183                &[KIND_KEY],
184                "Intent 'kind' must be a symbol",
185            ));
186        }
187        None => return Err(IntentError::at(&[], "Intent is missing a 'kind' tag")),
188    };
189    validate_origin(entries)?;
190    for required in required_fields(&kind.name) {
191        let Some(value) = entry(entries, required) else {
192            return Err(IntentError::at(
193                &[required],
194                format!("Intent '{kind}' is missing required field '{required}'"),
195            ));
196        };
197        if *required == "path" && !matches!(value, Expr::List(_)) {
198            return Err(IntentError::at(
199                &["path"],
200                "edit-field 'path' must be a list of segments",
201            ));
202        }
203    }
204    Ok(())
205}
206
207fn validate_origin(entries: &[(Expr, Expr)]) -> Result<(), IntentError> {
208    let Some(origin) = entry(entries, ORIGIN_KEY) else {
209        return Err(IntentError::at(
210            &[ORIGIN_KEY],
211            "Intent is missing an 'origin'",
212        ));
213    };
214    let Expr::Map(origin_entries) = origin else {
215        return Err(IntentError::at(
216            &[ORIGIN_KEY],
217            "Intent 'origin' must be a map",
218        ));
219    };
220    match entry(origin_entries, OPERATOR_KEY) {
221        Some(Expr::Symbol(symbol)) if Operator::from_name(&symbol.name).is_some() => {}
222        _ => {
223            return Err(IntentError::at(
224                &[ORIGIN_KEY, OPERATOR_KEY],
225                "origin 'operator' must be 'human' or 'agent'",
226            ));
227        }
228    }
229    match entry(origin_entries, AT_TICK_KEY) {
230        Some(Expr::Number(_)) => Ok(()),
231        _ => Err(IntentError::at(
232            &[ORIGIN_KEY, AT_TICK_KEY],
233            "origin 'at-tick' must be a number",
234        )),
235    }
236}
237
238/// Resolve every target an Intent references against `is_known`, returning a
239/// diagnostic for the first unknown target. A failed resolution means the
240/// editor must not produce an operation: nothing mutates.
241pub fn resolve_targets(expr: &Expr, is_known: impl Fn(&Expr) -> bool) -> Result<(), IntentError> {
242    for (label, target) in referenced_targets(expr) {
243        if !is_known(&target) {
244            return Err(IntentError {
245                path: vec![label],
246                message: "Intent references an unknown target".to_owned(),
247            });
248        }
249    }
250    Ok(())
251}
252
253/// The list of `(field-label, target-expr)` references an Intent carries, by
254/// kind. Port references (`from`/`to`) contribute their inner `node`.
255pub fn referenced_targets(expr: &Expr) -> Vec<(String, Expr)> {
256    let Some(kind) = intent_kind_of(expr) else {
257        return Vec::new();
258    };
259    let mut refs = Vec::new();
260    let mut single = |name: &str| {
261        if let Some(value) = field(expr, name) {
262            refs.push((name.to_owned(), value.clone()));
263        }
264    };
265    match &*kind.name {
266        "tap" | "edit-field" | "invoke" | "scrub" | "set-param" | "performance-event"
267        | "piano-roll-edit" | "player-rack-edit" | "arranger-edit" => single("target"),
268        "move" => single("node"),
269        "unwire" => single("edge"),
270        "create" => single("class"),
271        "open" => single("value"),
272        "approve" | "reject" | "ask" | "split-mission" | "pause-agent" | "rerun-validation"
273        | "replay-cassette" => single("mission"),
274        "open-source" => single("location"),
275        "select" | "delete" => {
276            if let Some(Expr::List(items)) = field(expr, "targets") {
277                for (index, item) in items.iter().enumerate() {
278                    refs.push((format!("targets[{index}]"), item.clone()));
279                }
280            }
281        }
282        "wire" => {
283            for end in ["from", "to"] {
284                if let Some(Expr::Map(port)) = field(expr, end)
285                    && let Some(node) = entry(port, "node")
286                {
287                    refs.push((format!("{end}.node"), node.clone()));
288                }
289            }
290        }
291        _ => {}
292    }
293    refs
294}