Skip to main content

dirtydata_core/
constitution.rs

1//! 憲法テスト — DirtyData の根本的不変条件。
2//!
3//! # Every state must be explainable, or disposable.
4//!
5//! これは property test である。
6//! ランダムな操作列に対して、以下が常に成立することを証明する:
7//!
8//! 1. Replayability: replay(patches) == current graph (hash一致)
9//! 2. Hash Stability: 同一 operations → 同一 hash
10//! 3. Explainability: graph.applied_patches は全て存在し、逆変換可能
11
12#[cfg(test)]
13mod tests {
14    use crate::hash;
15    use crate::ir::{Edge, Graph, Node};
16    use crate::patch::{Operation, Patch};
17    use crate::types::*;
18    use proptest::prelude::*;
19    use std::collections::BTreeMap;
20
21    // ── Strategy: ランダムなノード生成 ─────────
22
23    fn arb_node_kind() -> impl Strategy<Value = NodeKind> {
24        prop_oneof![
25            Just(NodeKind::Source),
26            Just(NodeKind::Processor),
27            Just(NodeKind::Analyzer),
28            Just(NodeKind::Sink),
29            Just(NodeKind::Junction),
30        ]
31    }
32
33    fn arb_node() -> impl Strategy<Value = Node> {
34        (arb_node_kind(), "[a-z]{3,8}").prop_map(|(kind, name)| {
35            let ports = match kind {
36                NodeKind::Source => vec![TypedPort {
37                    name: "out".into(),
38                    direction: PortDirection::Output,
39                    domain: ExecutionDomain::Sample,
40                    data_type: DataType::Audio { channels: 2 },
41                    semantic: PortSemantic::Signal,
42                    polarity: PortPolarity::Bipolar,
43                }],
44                NodeKind::Sink => vec![TypedPort {
45                    name: "in".into(),
46                    direction: PortDirection::Input,
47                    domain: ExecutionDomain::Sample,
48                    data_type: DataType::Audio { channels: 2 },
49                    semantic: PortSemantic::Signal,
50                    polarity: PortPolarity::Bipolar,
51                }],
52                _ => vec![
53                    TypedPort {
54                        name: "in".into(),
55                        direction: PortDirection::Input,
56                        domain: ExecutionDomain::Sample,
57                        data_type: DataType::Audio { channels: 2 },
58                        semantic: PortSemantic::Signal,
59                        polarity: PortPolarity::Bipolar,
60                    },
61                    TypedPort {
62                        name: "out".into(),
63                        direction: PortDirection::Output,
64                        domain: ExecutionDomain::Sample,
65                        data_type: DataType::Audio { channels: 2 },
66                        semantic: PortSemantic::Signal,
67                        polarity: PortPolarity::Bipolar,
68                    },
69                ],
70            };
71            let mut config = BTreeMap::new();
72            config.insert("name".into(), ConfigValue::String(name));
73            Node {
74                id: StableId::new(),
75                kind,
76                ports,
77                config,
78                metadata: MetadataRef(None),
79                confidence: ConfidenceScore::Verified,
80            }
81        })
82    }
83
84    fn arb_config_value() -> impl Strategy<Value = ConfigValue> {
85        prop_oneof![
86            any::<f64>().prop_map(ConfigValue::Float),
87            any::<i64>().prop_map(ConfigValue::Int),
88            any::<bool>().prop_map(ConfigValue::Bool),
89            "[a-z]{1,10}".prop_map(ConfigValue::String),
90        ]
91    }
92
93    // ── 憲法 1: Replayability Invariant ────────
94    //
95    // 任意のパッチ列を適用した結果の graph は、
96    // 同じパッチ列を replay することで完全に再構成できる。
97
98    proptest! {
99        #[test]
100        fn constitution_replayability(
101            node_count in 1usize..8,
102        ) {
103            // ランダムにノードを生成してパッチを作成
104            let rt = proptest::test_runner::TestRunner::new(Default::default());
105            let mut nodes = Vec::new();
106            for _ in 0..node_count {
107                // Use fixed nodes for determinism within proptest
108                nodes.push(Node::new_processor(&format!("node_{}", nodes.len())));
109            }
110
111            let patch = Patch::from_operations(
112                nodes.iter().map(|n| Operation::AddNode(n.clone())).collect()
113            );
114
115            // Apply
116            let mut graph = Graph::new();
117            graph.apply(&patch).unwrap();
118
119            // Replay
120            let replayed = Graph::replay(&[patch]).unwrap();
121
122            // 憲法: hash は一致しなければならない
123            let h1 = hash::hash_graph(&graph);
124            let h2 = hash::hash_graph(&replayed);
125            prop_assert_eq!(h1, h2, "Replayability violation: graph hashes differ");
126        }
127    }
128
129    // ── 憲法 2: Hash Stability Invariant ───────
130    //
131    // 同一の operations から生成された patch は
132    // 常に同一の deterministic_hash を持つ。
133
134    #[test]
135    fn constitution_hash_stability() {
136        // 同じ構造のノードを2回作る(IDは異なる)
137        // → hash は operations の内容に依存するので、
138        //   同一の Patch インスタンスは常に同一 hash を返す
139
140        let node = Node::new_processor("TestGain");
141        let ops = vec![Operation::AddNode(node.clone())];
142
143        let patch1 = Patch::from_operations(ops.clone());
144
145        // 同じ ops から再計算
146        let recomputed_hash = hash::hash_patch(&patch1);
147
148        assert_eq!(
149            patch1.deterministic_hash, recomputed_hash,
150            "Hash stability violation"
151        );
152    }
153
154    // ── 憲法 3: Explainability Invariant ───────
155    //
156    // graph.applied_patches の全エントリは
157    // 実際に適用されたパッチと1:1対応する。
158    // パッチ数 == applied_patches.len()
159
160    proptest! {
161        #[test]
162        fn constitution_explainability(
163            patch_count in 1usize..6,
164        ) {
165            let mut graph = Graph::new();
166            let mut all_patches = Vec::new();
167
168            for i in 0..patch_count {
169                let node = Node::new_processor(&format!("node_{}", i));
170                let patch = Patch::from_operations(vec![Operation::AddNode(node)]);
171                graph.apply(&patch).unwrap();
172                all_patches.push(patch);
173            }
174
175            // 憲法: applied_patches は全パッチの ID と一致
176            prop_assert_eq!(
177                graph.applied_patches.len(),
178                all_patches.len(),
179                "Explainability violation: patch count mismatch"
180            );
181
182            for (applied_id, patch) in graph.applied_patches.iter().zip(all_patches.iter()) {
183                prop_assert_eq!(
184                    applied_id, &patch.identity,
185                    "Explainability violation: patch ID mismatch"
186                );
187            }
188
189            // 憲法: replay から同一グラフを再構成可能
190            let replayed = Graph::replay(&all_patches).unwrap();
191            let h1 = hash::hash_graph(&graph);
192            let h2 = hash::hash_graph(&replayed);
193            prop_assert_eq!(h1, h2,
194                "Explainability violation: replay produces different state"
195            );
196        }
197    }
198
199    // ── 憲法 4: Modify-Replay Round-trip ───────
200    //
201    // config 変更を含むパッチ列でも replay は成立する。
202
203    #[test]
204    fn constitution_modify_replay() {
205        let node = Node::new_processor("Gain");
206        let p1 = Patch::from_operations(vec![Operation::AddNode(node.clone())]);
207
208        let mut delta = BTreeMap::new();
209        delta.insert(
210            "gain_db".into(),
211            ConfigChange {
212                old: None,
213                new: Some(ConfigValue::Float(3.5)),
214            },
215        );
216        let p2 = Patch::from_operations(vec![Operation::ModifyConfig {
217            node_id: node.id,
218            delta,
219        }]);
220
221        // Apply sequentially
222        let mut graph = Graph::new();
223        graph.apply(&p1).unwrap();
224        graph.apply(&p2).unwrap();
225
226        // Replay
227        let replayed = Graph::replay(&[p1, p2]).unwrap();
228
229        assert_eq!(
230            hash::hash_graph(&graph),
231            hash::hash_graph(&replayed),
232            "Modify-replay round-trip failed"
233        );
234    }
235
236    // ── 憲法 5: Edge Operations Replay ─────────
237
238    #[test]
239    fn constitution_edge_replay() {
240        let src = Node::new_source("Sine");
241        let gain = Node::new_processor("Gain");
242        let sink = Node::new_sink("Output");
243
244        let edge1 = Edge::new(
245            PortRef {
246                node_id: src.id,
247                port_name: "out".into(),
248            },
249            PortRef {
250                node_id: gain.id,
251                port_name: "in".into(),
252            },
253        );
254        let edge2 = Edge::new(
255            PortRef {
256                node_id: gain.id,
257                port_name: "out".into(),
258            },
259            PortRef {
260                node_id: sink.id,
261                port_name: "in".into(),
262            },
263        );
264
265        let p1 = Patch::from_operations(vec![
266            Operation::AddNode(src),
267            Operation::AddNode(gain),
268            Operation::AddNode(sink),
269        ]);
270        let p2 = Patch::from_operations(vec![Operation::AddEdge(edge1), Operation::AddEdge(edge2)]);
271
272        let mut graph = Graph::new();
273        graph.apply(&p1).unwrap();
274        graph.apply(&p2).unwrap();
275
276        let replayed = Graph::replay(&[p1, p2]).unwrap();
277
278        assert_eq!(
279            hash::hash_graph(&graph),
280            hash::hash_graph(&replayed),
281            "Edge replay round-trip failed"
282        );
283    }
284}