1pub mod attribution;
9
10pub use attribution::*;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::Path;
14
15use dirtydata_core::types::*;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "strategy", rename_all = "snake_case")]
20pub enum IntentStrategy {
21 Manual,
23 InsertNode {
25 kind: NodeKind,
26 name: String,
27 config: ConfigSnapshot
28 },
29 Bridge {
31 from_node: String,
32 to_node: String
33 },
34 Freeze {
36 target_node: String
37 },
38}
39
40impl Default for IntentStrategy {
41 fn default() -> Self {
42 Self::Manual
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct IntentNode {
49 pub id: IntentId,
50 pub description: String,
51 pub constraints: Vec<IntentConstraint>,
52 pub status: IntentStatus,
53 pub strategy: IntentStrategy,
54 pub attached_patches: Vec<PatchId>,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59pub struct IntentState {
60 pub intents: HashMap<IntentId, IntentNode>,
61}
62
63impl IntentState {
64 pub fn save(&self, project_root: &Path) -> Result<(), std::io::Error> {
65 let path = project_root.join(".dirtydata").join("intents.json");
66 let data = serde_json::to_string_pretty(self)?;
67 std::fs::write(path, data)
68 }
69
70 pub fn load(project_root: &Path) -> Result<Self, std::io::Error> {
71 let path = project_root.join(".dirtydata").join("intents.json");
72 if !path.exists() {
73 return Ok(Self::default());
74 }
75 let data = std::fs::read_to_string(path)?;
76 let state = serde_json::from_str(&data)?;
77 Ok(state)
78 }
79
80 pub fn add(&mut self, description: String, constraints: Vec<IntentConstraint>) -> IntentId {
81 let id = IntentId::new();
82 self.intents.insert(
83 id,
84 IntentNode {
85 id,
86 description,
87 constraints,
88 status: IntentStatus::Proposal,
89 strategy: IntentStrategy::Manual,
90 attached_patches: Vec::new(),
91 },
92 );
93 id
94 }
95
96 pub fn attach(&mut self, id: IntentId, patch_id: PatchId) -> Result<(), String> {
97 let intent = self
98 .intents
99 .get_mut(&id)
100 .ok_or_else(|| format!("Intent {} not found", id))?;
101 if !intent.attached_patches.contains(&patch_id) {
102 intent.attached_patches.push(patch_id);
103 }
104 if intent.status == IntentStatus::Proposal {
105 intent.status = IntentStatus::Attached;
106 }
107 Ok(())
108 }
109
110 pub fn evaluate_constraints(&self, id: IntentId, graph: &dirtydata_core::ir::Graph) -> Vec<String> {
113 let intent = match self.intents.get(&id) {
114 Some(i) => i,
115 None => return vec![format!("Intent {} not found", id)],
116 };
117 let mut violations = Vec::new();
118
119 for constraint in &intent.constraints {
120 match constraint {
121 IntentConstraint::Must(bound) => {
122 if let Some(val) = self.get_param_value(graph, &bound.target) {
123 if val < bound.range_start || val > bound.range_end {
124 violations.push(format!("Constraint Violation [Must]: {} is {}, but must be in range {}..={}", bound.target, val, bound.range_start, bound.range_end));
125 }
126 }
127 }
128 IntentConstraint::Never(bound) => {
129 if let Some(val) = self.get_param_value(graph, &bound.target) {
130 if val >= bound.range_start && val <= bound.range_end {
131 violations.push(format!("Constraint Violation [Never]: {} is {}, which is forbidden in range {}..={}", bound.target, val, bound.range_start, bound.range_end));
132 }
133 }
134 }
135 _ => {} }
137 }
138 violations
139 }
140
141 fn get_param_value(&self, graph: &dirtydata_core::ir::Graph, path: &str) -> Option<f32> {
142 let parts: Vec<&str> = path.split('.').collect();
143 if parts.len() != 2 { return None; }
144
145 let node_name = parts[0];
146 let param_key = parts[1];
147
148 for node in graph.nodes.values() {
149 if dirtydata_core::actions::node_name(node) == node_name {
150 if let Some(ConfigValue::Float(f)) = node.config.get(param_key) {
151 return Some(*f as f32);
152 }
153 }
154 }
155 None
156 }
157}