pub mod attribution;
pub use attribution::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use dirtydata_core::types::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "strategy", rename_all = "snake_case")]
pub enum IntentStrategy {
Manual,
InsertNode {
kind: NodeKind,
name: String,
config: ConfigSnapshot
},
Bridge {
from_node: String,
to_node: String
},
Freeze {
target_node: String
},
}
impl Default for IntentStrategy {
fn default() -> Self {
Self::Manual
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntentNode {
pub id: IntentId,
pub description: String,
pub constraints: Vec<IntentConstraint>,
pub status: IntentStatus,
pub strategy: IntentStrategy,
pub attached_patches: Vec<PatchId>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IntentState {
pub intents: HashMap<IntentId, IntentNode>,
}
impl IntentState {
pub fn save(&self, project_root: &Path) -> Result<(), std::io::Error> {
let path = project_root.join(".dirtydata").join("intents.json");
let data = serde_json::to_string_pretty(self)?;
std::fs::write(path, data)
}
pub fn load(project_root: &Path) -> Result<Self, std::io::Error> {
let path = project_root.join(".dirtydata").join("intents.json");
if !path.exists() {
return Ok(Self::default());
}
let data = std::fs::read_to_string(path)?;
let state = serde_json::from_str(&data)?;
Ok(state)
}
pub fn add(&mut self, description: String, constraints: Vec<IntentConstraint>) -> IntentId {
let id = IntentId::new();
self.intents.insert(
id,
IntentNode {
id,
description,
constraints,
status: IntentStatus::Proposal,
strategy: IntentStrategy::Manual,
attached_patches: Vec::new(),
},
);
id
}
pub fn attach(&mut self, id: IntentId, patch_id: PatchId) -> Result<(), String> {
let intent = self
.intents
.get_mut(&id)
.ok_or_else(|| format!("Intent {} not found", id))?;
if !intent.attached_patches.contains(&patch_id) {
intent.attached_patches.push(patch_id);
}
if intent.status == IntentStatus::Proposal {
intent.status = IntentStatus::Attached;
}
Ok(())
}
pub fn evaluate_constraints(&self, id: IntentId, graph: &dirtydata_core::ir::Graph) -> Vec<String> {
let intent = match self.intents.get(&id) {
Some(i) => i,
None => return vec![format!("Intent {} not found", id)],
};
let mut violations = Vec::new();
for constraint in &intent.constraints {
match constraint {
IntentConstraint::Must(bound) => {
if let Some(val) = self.get_param_value(graph, &bound.target) {
if val < bound.range_start || val > bound.range_end {
violations.push(format!("Constraint Violation [Must]: {} is {}, but must be in range {}..={}", bound.target, val, bound.range_start, bound.range_end));
}
}
}
IntentConstraint::Never(bound) => {
if let Some(val) = self.get_param_value(graph, &bound.target) {
if val >= bound.range_start && val <= bound.range_end {
violations.push(format!("Constraint Violation [Never]: {} is {}, which is forbidden in range {}..={}", bound.target, val, bound.range_start, bound.range_end));
}
}
}
_ => {} }
}
violations
}
fn get_param_value(&self, graph: &dirtydata_core::ir::Graph, path: &str) -> Option<f32> {
let parts: Vec<&str> = path.split('.').collect();
if parts.len() != 2 { return None; }
let node_name = parts[0];
let param_key = parts[1];
for node in graph.nodes.values() {
if dirtydata_core::actions::node_name(node) == node_name {
if let Some(ConfigValue::Float(f)) = node.config.get(param_key) {
return Some(*f as f32);
}
}
}
None
}
}