Skip to main content

dirtydata_intent/
lib.rs

1//! Intent Engine — Structured Meaning & Constraints
2//!
3//! "音楽は最適化問題じゃない。制約付き妥協問題です。"
4//!
5//! DirtyData において、パッチは単なる状態変更の羅列ではない。
6//! Intent(意図)という上位概念があり、パッチは「それを実現するための Strategy の結果」である。
7
8pub 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/// Intent の実現方法。
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "strategy", rename_all = "snake_case")]
20pub enum IntentStrategy {
21    /// 手動。ユーザーがパッチを適用して紐付ける。
22    Manual,
23    /// 自動。特定のノードを挿入する。
24    InsertNode { 
25        kind: NodeKind, 
26        name: String,
27        config: ConfigSnapshot 
28    },
29    /// 自動。既存のノードを接続する。
30    Bridge { 
31        from_node: String, 
32        to_node: String 
33    },
34    /// 自動。安全な Frozen Asset に置換する。
35    Freeze { 
36        target_node: String 
37    },
38}
39
40impl Default for IntentStrategy {
41    fn default() -> Self {
42        Self::Manual
43    }
44}
45
46/// Intent 本体。何を実現したいか。
47#[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/// IntentEngine の永続状態。
58#[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    /// 制約を評価し、違反内容を返す。
111    /// これが Semantic Timeline で「なぜ壊れたか」を表示する基盤となる。
112    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                _ => {} // Prefer/Avoid are soft
136            }
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}