Skip to main content

dirtydata_core/
actions.rs

1//! User-facing Action Schema.
2//!
3//! Authoring Language ではなく Review Language。
4//! 吸うな。その薬は強い。export-only。
5
6use serde::{Deserialize, Serialize};
7use crate::ir::{Edge, Graph, Node};
8use crate::patch::Operation;
9use crate::types::*;
10
11/// User-facing action — what humans write.
12/// Internal operations are derived from these.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(tag = "action", rename_all = "snake_case")]
15pub enum UserAction {
16    AddSource { name: String, #[serde(default = "default_channels")] channels: u32 },
17    AddProcessor { name: String, #[serde(default = "default_channels")] channels: u32 },
18    AddAnalyzer { name: String, #[serde(default = "default_channels")] channels: u32 },
19    AddSink { name: String, #[serde(default = "default_channels")] channels: u32 },
20    AddForeign { name: String, plugin: String, #[serde(default = "default_channels")] channels: u32 },
21    Connect { from: String, from_port: Option<String>, to: String, to_port: Option<String> },
22    Disconnect { from: String, from_port: Option<String>, to: String, to_port: Option<String> },
23    RemoveNode { name: String },
24    FreezeNode { name: String, length_secs: f32 },
25    SetConfig { node: String, key: String, value: serde_json::Value },
26    AddModulation { source_node: String, source_port: String, target_node: String, target_param: String, amount: f32 },
27    RemoveModulation { id: StableId },
28    AddSubGraph { name: String },
29    ReplaceNode { name: String, new_kind_name: String },
30    DuplicateNode { node_id: StableId },
31    CheckoutRevision { revision: PatchId },
32    SquashHistory,
33    CreateSnapshot { name: String },
34    RunMonteCarlo { node_name: String, count: usize },
35    RunSensitivity { node_name: String },
36    RunStabilityMap { node_name: String, param: String, range_start: f32, range_end: f32 },
37}
38
39fn default_channels() -> u32 { 2 }
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct UserPatchFile {
43    pub description: Option<String>,
44    pub intent: Option<String>,
45    pub constraints: Vec<UserConstraint>,
46    pub actions: Vec<UserAction>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct UserConstraint {
51    #[serde(rename = "type")]
52    pub kind: String,
53    pub description: String,
54}
55
56#[derive(Debug, thiserror::Error)]
57pub enum ActionError {
58    #[error("node '{0}' not found")] NodeNotFound(String),
59    #[error("ambiguous node name '{0}'")] AmbiguousName(String),
60    #[error("invalid config for '{0}': {1}")] InvalidConfig(String, String),
61}
62
63pub fn compile_actions(actions: &[UserAction], graph: &Graph) -> Result<Vec<Operation>, ActionError> {
64    let mut ops = Vec::new();
65    let mut created: std::collections::HashMap<String, StableId> = std::collections::HashMap::new();
66
67    for action in actions {
68        match action {
69            UserAction::AddSource { name, channels } => {
70                let node = make_node(NodeKind::Source, name, *channels);
71                created.insert(name.clone(), node.id);
72                ops.push(Operation::AddNode(node));
73            }
74            UserAction::AddProcessor { name, channels } => {
75                let node = make_node(NodeKind::Processor, name, *channels);
76                created.insert(name.clone(), node.id);
77                ops.push(Operation::AddNode(node));
78            }
79            UserAction::AddAnalyzer { name, channels } => {
80                let node = make_node(NodeKind::Analyzer, name, *channels);
81                created.insert(name.clone(), node.id);
82                ops.push(Operation::AddNode(node));
83            }
84            UserAction::AddSink { name, channels } => {
85                let node = make_node(NodeKind::Sink, name, *channels);
86                created.insert(name.clone(), node.id);
87                ops.push(Operation::AddNode(node));
88            }
89            UserAction::AddForeign { name, plugin, channels } => {
90                let node = make_node(NodeKind::Foreign(plugin.clone()), name, *channels);
91                created.insert(name.clone(), node.id);
92                ops.push(Operation::AddNode(node));
93            }
94            UserAction::Connect { from, from_port, to, to_port } => {
95                let src_id = resolve_name(from, graph, &created)?;
96                let tgt_id = resolve_name(to, graph, &created)?;
97                let edge = Edge::new(
98                    PortRef { node_id: src_id, port_name: from_port.clone().unwrap_or_else(|| "out".into()) },
99                    PortRef { node_id: tgt_id, port_name: to_port.clone().unwrap_or_else(|| "in".into()) },
100                );
101                ops.push(Operation::AddEdge(edge));
102            }
103            UserAction::Disconnect { from, from_port, to, to_port } => {
104                let src_id = resolve_name(from, graph, &created)?;
105                let tgt_id = resolve_name(to, graph, &created)?;
106                let src_port = from_port.clone().unwrap_or_else(|| "out".into());
107                let tgt_port = to_port.clone().unwrap_or_else(|| "in".into());
108                if let Some(edge_id) = graph.edges.values().find_map(|e| {
109                    if e.source.node_id == src_id && e.source.port_name == src_port && e.target.node_id == tgt_id && e.target.port_name == tgt_port { Some(e.id) } else { None }
110                }) {
111                    ops.push(Operation::RemoveEdge(edge_id));
112                }
113            }
114            UserAction::RemoveNode { name } => {
115                let id = resolve_name(name, graph, &created)?;
116                ops.push(Operation::RemoveNode(id));
117            }
118            UserAction::SetConfig { node, key, value } => {
119                let id = resolve_name(node, graph, &created)?;
120                let config_val = json_to_config_value(value).map_err(|e| ActionError::InvalidConfig(key.clone(), e))?;
121                let mut delta = std::collections::BTreeMap::new();
122                delta.insert(key.clone(), ConfigChange { old: None, new: Some(config_val) });
123                ops.push(Operation::ModifyConfig { node_id: id, delta });
124            }
125            UserAction::AddModulation { source_node, source_port, target_node, target_param, amount } => {
126                let src_id = resolve_name(source_node, graph, &created)?;
127                let tgt_id = resolve_name(target_node, graph, &created)?;
128                let mod_ir = crate::ir::Modulation::new(
129                    PortRef { node_id: src_id, port_name: source_port.clone() },
130                    tgt_id, target_param.clone(), *amount,
131                );
132                ops.push(Operation::AddModulation(mod_ir));
133            }
134            UserAction::RemoveModulation { id } => { ops.push(Operation::RemoveModulation(*id)); }
135            UserAction::AddSubGraph { name } => {
136                let node = crate::ir::Node::new_subgraph(name);
137                created.insert(name.clone(), node.id);
138                ops.push(Operation::AddNode(node));
139            }
140            UserAction::ReplaceNode { name, new_kind_name } => {
141                let id = resolve_name(name, graph, &created)?;
142                let mut delta = std::collections::BTreeMap::new();
143                delta.insert("name".to_string(), ConfigChange { old: None, new: Some(ConfigValue::String(new_kind_name.clone())) });
144                ops.push(Operation::ModifyConfig { node_id: id, delta });
145            }
146            UserAction::DuplicateNode { .. } | UserAction::FreezeNode { .. } | UserAction::CheckoutRevision { .. } | UserAction::SquashHistory | UserAction::CreateSnapshot { .. } => {}
147            UserAction::RunMonteCarlo { node_name, count } => {
148                let id = resolve_name(node_name, graph, &created)?;
149                if let Some(node) = graph.nodes.get(&id) {
150                    for _ in 0..*count {
151                        for (key, value) in &node.config {
152                            if let ConfigValue::Float(f) = value {
153                                let noise = (rand::random::<f64>() - 0.5) * 0.1;
154                                let mut delta = std::collections::BTreeMap::new();
155                                delta.insert(key.clone(), ConfigChange { old: None, new: Some(ConfigValue::Float(*f + noise)) });
156                                ops.push(Operation::ModifyConfig { node_id: id, delta });
157                            }
158                        }
159                    }
160                }
161            }
162            UserAction::RunSensitivity { .. } | UserAction::RunStabilityMap { .. } => {}
163        }
164    }
165    Ok(ops)
166}
167
168pub fn node_name(node: &Node) -> String {
169    node.config.get("name").and_then(|v| v.as_string()).cloned().unwrap_or_else(|| node.id.to_string())
170}
171
172fn make_node(kind: NodeKind, name: &str, _channels: u32) -> Node {
173    let mut n = match kind {
174        NodeKind::Source => Node::new_source(name),
175        NodeKind::Processor => Node::new_processor(name),
176        NodeKind::Sink => Node::new_sink(name),
177        _ => Node::new_processor(name),
178    };
179    n.kind = kind;
180    // Adjust channels if needed... (stub)
181    n
182}
183
184pub fn resolve_name(name: &str, graph: &Graph, created: &std::collections::HashMap<String, StableId>) -> Result<StableId, ActionError> {
185    if let Some(&id) = created.get(name) { return Ok(id); }
186    if let Ok(id) = name.parse::<StableId>() { if graph.nodes.contains_key(&id) { return Ok(id); } }
187    let matches: Vec<StableId> = graph.nodes.iter().filter(|(_, n)| node_name(n) == name).map(|(&id, _)| id).collect();
188    match matches.len() {
189        0 => Err(ActionError::NodeNotFound(name.into())),
190        1 => Ok(matches[0]),
191        _ => Err(ActionError::AmbiguousName(name.into())),
192    }
193}
194
195fn json_to_config_value(v: &serde_json::Value) -> Result<ConfigValue, String> {
196    match v {
197        serde_json::Value::Number(n) => Ok(ConfigValue::Float(n.as_f64().unwrap_or(0.0))),
198        serde_json::Value::Bool(b) => Ok(ConfigValue::Bool(*b)),
199        serde_json::Value::String(s) => Ok(ConfigValue::String(s.clone())),
200        serde_json::Value::Array(arr) => {
201            let items: Result<Vec<_>, _> = arr.iter().map(json_to_config_value).collect();
202            Ok(ConfigValue::List(items?))
203        }
204        serde_json::Value::Object(map) => {
205            let mut bmap = std::collections::BTreeMap::new();
206            for (k, v) in map { bmap.insert(k.clone(), json_to_config_value(v)?); }
207            Ok(ConfigValue::Map(bmap))
208        }
209        serde_json::Value::Null => Err("null is not allowed".into()),
210    }
211}