Skip to main content

evorule_reactor/
hash.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 EvoRule Project
3// This file is part of EvoRule, licensed under GNU Affero General Public License v3 or later.
4//! 审计链哈希工具(BLAKE3)
5//!
6//! 这是 evorule 审计链的核心完整性保证机制。
7//!
8//! # 设计
9//! - 使用 `blake3` crate(1.x)计算 256 位哈希
10//! - 序列化采用显式 JSON 格式:确保序列化格式的稳定性,不受 Debug 实现变更影响
11//! - 哈希以十六进制字符串形式返回
12//!
13//! # 哈希链算法
14//! - `prev_hash` 初始为 `"genesis"`
15//! - 对每个 Fact,计算 `content_hash = blake3(fact_to_stable_json(fact))`
16//! - 链哈希:`chain_hash = blake3(prev_hash + content_hash)`
17//! - 更新 `prev_hash = chain_hash`,继续处理下一个 Fact
18//!
19//! # 两套 WAL 合并
20//! 本模块是哈希算法的**单一真相源**(single source of truth)。
21//! - evorule-governance/src/hash.rs re-export 本模块
22//! - evorule-cli/src/hash.rs re-export 本模块
23//! - 通过 `test_cross_validate_with_tier2` 测试保证三方一致
24
25use std::fmt;
26
27use evorule_tcb::JsonValue;
28use tracing::{debug, trace};
29
30use crate::fact::Fact;
31
32/// 哈希计算错误类型
33///
34/// 包含错误消息,便于排查哈希计算过程中的异常。
35#[derive(Debug)]
36pub struct HashError {
37    message: String,
38}
39
40impl HashError {
41    fn new(message: impl Into<String>) -> Self {
42        Self {
43            message: message.into(),
44        }
45    }
46}
47
48impl fmt::Display for HashError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "哈希计算错误: {}", self.message)
51    }
52}
53
54impl std::error::Error for HashError {}
55
56/// 将 `JsonValue` 转换为 `serde_json::Value`
57///
58/// 内部函数,用于 Fact 序列化前的类型转换。
59/// 保证转换过程确定性,不受 JsonValue 内部实现影响。
60fn tcb_to_serde(value: &JsonValue) -> serde_json::Value {
61    match value {
62        JsonValue::Null => serde_json::Value::Null,
63        JsonValue::Bool(b) => serde_json::Value::Bool(*b),
64        JsonValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
65        JsonValue::String(s) => serde_json::Value::String(s.to_string()),
66        JsonValue::Array(arr) => serde_json::Value::Array(arr.iter().map(tcb_to_serde).collect()),
67        JsonValue::Object(obj) => {
68            let mut map = serde_json::Map::new();
69            for (k, v) in obj.iter() {
70                map.insert(k.clone(), tcb_to_serde(v));
71            }
72            serde_json::Value::Object(map)
73        }
74    }
75}
76
77/// 将 Fact 序列化为稳定的 JSON 格式
78///
79/// 使用显式的 JSON 序列化而非依赖 Debug trait,确保:
80/// 1. 序列化格式不受 Rust 版本或 Debug 实现变更影响
81/// 2. 字段顺序固定且可预测(serde_json::Map 基于 BTreeMap,字母序)
82/// 3. 跨版本兼容性有保障
83///
84/// # 参数
85/// - `fact`: 要序列化的事实
86///
87/// # 返回值
88/// - `Ok(serde_json::Value)`: 序列化后的 JSON 值
89/// - `Err(HashError)`: 序列化失败
90// 7 种 Fact 变体扁平 match + 嵌套, 拆函数需共享中间变量。详见 GATE_REFERENCE.md §六(豁免索引)
91#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
92pub fn fact_to_stable_json(fact: &Fact) -> Result<serde_json::Value, HashError> {
93    let fact_type = fact.type_name();
94    let fact_id = fact.id();
95
96    trace!(
97        事实ID = ?fact_id,
98        事实类型 = %fact_type,
99        "序列化开始"
100    );
101
102    let mut obj = serde_json::Map::new();
103    match fact {
104        Fact::Command { id, instruction } => {
105            trace!(
106                事实ID = ?id,
107                指令大小 = instruction.to_string().len(),
108                "处理命令类型事实"
109            );
110            obj.insert("type".into(), serde_json::Value::String("Command".into()));
111            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
112            obj.insert("instruction".into(), tcb_to_serde(instruction));
113        }
114        Fact::PayloadUpdate { id, path, value } => {
115            trace!(
116                事实ID = ?id,
117                路径 = %path,
118                "处理载荷更新类型事实"
119            );
120            obj.insert(
121                "type".into(),
122                serde_json::Value::String("PayloadUpdate".into()),
123            );
124            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
125            obj.insert("path".into(), serde_json::Value::String(path.clone()));
126            obj.insert("value".into(), tcb_to_serde(value));
127        }
128        Fact::StateTransition {
129            id,
130            cause,
131            new_payload,
132            new_queue,
133        } => {
134            trace!(
135                事实ID = ?id,
136                原因ID = ?cause,
137                队列长度 = new_queue.len(),
138                "处理状态转换类型事实"
139            );
140            obj.insert(
141                "type".into(),
142                serde_json::Value::String("StateTransition".into()),
143            );
144            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
145            obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
146            obj.insert("new_payload".into(), tcb_to_serde(new_payload));
147            obj.insert(
148                "new_queue".into(),
149                serde_json::Value::Array(new_queue.iter().map(tcb_to_serde).collect()),
150            );
151        }
152        Fact::IoRequest {
153            id,
154            cause,
155            io_type,
156            params,
157        } => {
158            trace!(
159                事实ID = ?id,
160                原因ID = ?cause,
161                IO类型 = %io_type.as_str(),
162                "处理IO请求类型事实"
163            );
164            obj.insert("type".into(), serde_json::Value::String("IoRequest".into()));
165            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
166            obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
167            obj.insert(
168                "io_type".into(),
169                serde_json::Value::String(io_type.as_str().into()),
170            );
171            obj.insert("params".into(), tcb_to_serde(params));
172        }
173        Fact::IoResponse {
174            id,
175            request_id,
176            result,
177            error,
178        } => {
179            trace!(
180                事实ID = ?id,
181                请求ID = ?request_id,
182                是否有错误 = error.is_some(),
183                "处理IO响应类型事实"
184            );
185            obj.insert(
186                "type".into(),
187                serde_json::Value::String("IoResponse".into()),
188            );
189            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
190            obj.insert(
191                "request_id".into(),
192                serde_json::Value::Number(request_id.0.into()),
193            );
194            obj.insert("result".into(), tcb_to_serde(result));
195            obj.insert(
196                "error".into(),
197                error
198                    .as_ref()
199                    .map(|e| serde_json::Value::String(e.clone()))
200                    .unwrap_or(serde_json::Value::Null),
201            );
202        }
203        Fact::Stable { id, version } => {
204            trace!(事实ID = ?id, 版本 = version, "处理稳定状态类型事实");
205            obj.insert("type".into(), serde_json::Value::String("Stable".into()));
206            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
207            obj.insert(
208                "version".into(),
209                serde_json::Value::Number((*version).into()),
210            );
211        }
212        Fact::Error { id, message } => {
213            trace!(
214                事实ID = ?id,
215                消息 = %message,
216                "处理错误类型事实"
217            );
218            obj.insert("type".into(), serde_json::Value::String("Error".into()));
219            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
220            obj.insert("message".into(), serde_json::Value::String(message.clone()));
221        }
222        Fact::TransitionTrace {
223            id,
224            cause,
225            rule_hits,
226        } => {
227            // 归因轨迹参与哈希链(审计链防篡改覆盖归因事实)
228            trace!(事实ID = ?id, 原因ID = ?cause, 命中数 = rule_hits.len(), "处理命中归因轨迹事实");
229            obj.insert(
230                "type".into(),
231                serde_json::Value::String("TransitionTrace".into()),
232            );
233            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
234            obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
235            let hits: Vec<serde_json::Value> = rule_hits
236                .iter()
237                .map(|h| {
238                    serde_json::json!({
239                        "index": h.index,
240                        "instr_type": h.instr_type,
241                        "hit": h.hit,
242                    })
243                })
244                .collect();
245            obj.insert("rule_hits".into(), serde_json::Value::Array(hits));
246        }
247        Fact::Violation {
248            id,
249            cause,
250            rule_index,
251            reason,
252            instruction,
253        } => {
254            // 违规拦截事实参与哈希链(审计链防篡改覆盖拦截记录;UV-147)
255            trace!(事实ID = ?id, 原因ID = ?cause, 规则下标 = rule_index, "处理违规拦截事实");
256            obj.insert("type".into(), serde_json::Value::String("Violation".into()));
257            obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
258            obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
259            obj.insert(
260                "rule_index".into(),
261                serde_json::Value::Number((*rule_index).into()),
262            );
263            obj.insert("reason".into(), serde_json::Value::String(reason.clone()));
264            obj.insert("instruction".into(), tcb_to_serde(instruction));
265        }
266    }
267
268    let value = serde_json::Value::Object(obj);
269
270    trace!(
271        事实ID = ?fact_id,
272        事实类型 = %fact_type,
273        "序列化完成"
274    );
275
276    Ok(value)
277}
278
279/// 计算内容的 BLAKE3 哈希
280///
281/// 将 `JsonValue` 序列化为确定性字符串后计算哈希。
282/// 使用 `tcb_to_serde` 进行显式 JSON 序列化,确保格式稳定性。
283///
284/// # 参数
285/// - `value`: 要计算哈希的 JSON 值
286///
287/// # 返回值
288/// - `Ok(String)`: 64 字符的十六进制哈希字符串
289/// - `Err(HashError)`: 序列化失败
290#[allow(dead_code)] // 供 evorule-governance re-export 使用
291pub fn content_hash(value: &JsonValue) -> Result<String, HashError> {
292    #[cfg(kani)]
293    {
294        // Kani 模式:blake3 的位操作循环会导致 CBMC memcmp 状态爆炸
295        // (4357+ 次循环展开未止)。用确定性简化哈希替代,保持幂等性。
296        // 注意:不用 format!(触发 core::unicode::skip_search 状态爆炸)。
297        Ok(String::from("c"))
298    }
299    #[cfg(not(kani))]
300    {
301        let serde_value = tcb_to_serde(value);
302        let serialized = serde_json::to_string(&serde_value)
303            .map_err(|e| HashError::new(format!("内容哈希序列化失败: {}", e)))?;
304        let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
305
306        trace!(
307            序列化长度 = serialized.len(),
308            哈希值 = %hash,
309            "内容哈希计算完成"
310        );
311
312        Ok(hash)
313    }
314}
315
316/// 计算 Fact 的哈希(基于稳定的 JSON 序列化)
317///
318/// 使用显式 JSON 序列化格式而非 Debug trait,确保序列化格式的稳定性。
319///
320/// # 参数
321/// - `fact`: 要计算哈希的事实
322///
323/// # 返回值
324/// - `Ok(String)`: 64 字符的十六进制哈希字符串
325/// - `Err(HashError)`: 哈希计算失败
326///
327/// # 示例
328/// ```
329/// use evorule_reactor::{Fact, FactId, fact_hash};
330/// use evorule_tcb::JsonValue;
331///
332/// let fact = Fact::Command {
333///     id: FactId(1),
334///     instruction: JsonValue::empty_object(),
335/// };
336/// let hash = fact_hash(&fact).unwrap();
337/// assert_eq!(hash.len(), 64);
338/// ```
339pub fn fact_hash(fact: &Fact) -> Result<String, HashError> {
340    #[cfg(kani)]
341    {
342        // Kani 模式:blake3 的位操作循环会导致 CBMC memcmp 状态爆炸。
343        // 用确定性简化哈希替代:基于 id 的单字符映射(a-g),
344        // 保持幂等性(相同 id → 相同 hash)和区分性(不同 id → 不同 hash)。
345        // 不用 format!/to_string()/type_name(触发 Unicode 状态爆炸)。
346        let id = fact.id().0;
347        let h = match id % 7 {
348            0 => "a",
349            1 => "b",
350            2 => "c",
351            3 => "d",
352            4 => "e",
353            5 => "f",
354            _ => "g",
355        };
356        Ok(String::from(h))
357    }
358    #[cfg(not(kani))]
359    {
360        let fact_type = fact.type_name();
361        let fact_id = fact.id();
362
363        debug!(
364            事实ID = ?fact_id,
365            事实类型 = %fact_type,
366            "开始计算事实哈希"
367        );
368
369        let value = fact_to_stable_json(fact)?;
370        let serialized = serde_json::to_string(&value)
371            .map_err(|e| HashError::new(format!("序列化失败: {}", e)))?;
372
373        let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
374
375        debug!(
376            事实ID = ?fact_id,
377            事实类型 = %fact_type,
378            哈希值 = %hash,
379            序列化长度 = serialized.len(),
380            "事实哈希计算完成"
381        );
382
383        Ok(hash)
384    }
385}
386
387/// 计算哈希链的最终链哈希
388///
389/// 这是审计链的核心计算函数。
390///
391/// # 算法
392/// - `prev_hash` 初始为 `"genesis"`
393/// - 对每个 Fact,计算 `content_hash = fact_hash(fact)`
394/// - 链哈希:`chain_hash = blake3(prev_hash + content_hash)`
395/// - 更新 `prev_hash = chain_hash`,继续处理下一个 Fact
396/// - 返回最终的 `prev_hash`(即整条链的链哈希)
397///
398/// # 参数
399/// - `facts`: 事实列表(按因果顺序)
400///
401/// # 返回值
402/// - `Ok(String)`: 最终链哈希(64 字符十六进制)
403/// - `Err(HashError)`: 任一 Fact 哈希计算失败
404///
405/// # 注意
406/// 本函数**只计算**链哈希,**不验证**完整性。
407/// 验证逻辑由调用方比对存储的哈希与重算的哈希。
408/// 空列表返回 `"genesis"`。
409pub fn compute_chain_hash(facts: &[Fact]) -> Result<String, HashError> {
410    let fact_count = facts.len();
411
412    debug!(事实数量 = fact_count, "开始计算链哈希");
413
414    if fact_count == 0 {
415        debug!("事实列表为空,返回 genesis 哈希");
416        return Ok(String::from("genesis"));
417    }
418
419    let mut prev_hash = String::from("genesis");
420
421    trace!(
422        初始前序哈希 = %prev_hash,
423        "初始化前序哈希为创世值"
424    );
425
426    for (index, fact) in facts.iter().enumerate() {
427        let fact_type = fact.type_name();
428        let fact_id = fact.id();
429
430        trace!(
431            索引 = index,
432            事实ID = ?fact_id,
433            事实类型 = %fact_type,
434            前序哈希 = %prev_hash,
435            "处理事实"
436        );
437
438        let fh = fact_hash(fact)?;
439
440        trace!(
441            索引 = index,
442            事实ID = ?fact_id,
443            事实哈希 = %fh,
444            "事实哈希计算完成"
445        );
446
447        let current = chain_step(&prev_hash, &fh);
448
449        trace!(
450            索引 = index,
451            事实ID = ?fact_id,
452            当前哈希 = %current,
453            "计算当前哈希(前序哈希+事实哈希)"
454        );
455
456        prev_hash = current;
457    }
458
459    debug!(
460        事实数量 = fact_count,
461        最终哈希 = %prev_hash,
462        "链哈希计算完成"
463    );
464
465    Ok(prev_hash)
466}
467
468/// 计算单步链哈希:chain_hash = blake3(prev_hash + content_hash)
469///
470/// Kani 模式下用确定性简化版本(字符串拼接)替代 blake3,
471/// 避免 CBMC 对 blake3 位操作循环的 memcmp 状态爆炸。
472/// 保持链步的结合性:`chain_step(chain_step(a, b), c)` 可归纳分解。
473///
474/// # 参数
475/// - `prev_hash`: 前序链哈希(初始为 `"genesis"`)
476/// - `content_hash`: 当前 Fact 的内容哈希
477///
478/// # 返回值
479/// 新的链哈希
480pub fn chain_step(prev_hash: &str, content_hash: &str) -> String {
481    #[cfg(kani)]
482    {
483        // Kani 模式:确定性简化,保持链步结构。
484        // 不用 format!(触发 core::unicode::skip_search 状态爆炸),
485        // 用 String::from + push_str(字节级操作,短字符串)。
486        let mut s = String::from(prev_hash);
487        s.push_str(content_hash);
488        s
489    }
490    #[cfg(not(kani))]
491    {
492        let combined = format!("{}{}", prev_hash, content_hash);
493        blake3::hash(combined.as_bytes()).to_hex().to_string()
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    #![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
500    use super::*;
501    use crate::fact::{Fact, FactId, IoType};
502    use evorule_tcb::JsonValue;
503
504    #[test]
505    fn test_fact_to_stable_json_format() {
506        let command = Fact::Command {
507            id: FactId(1),
508            instruction: JsonValue::object_from_pairs(&[
509                ("type", JsonValue::string("increment")),
510                (
511                    "params",
512                    JsonValue::object_from_pairs(&[
513                        ("attr", JsonValue::string("x")),
514                        ("delta", JsonValue::Integer(5)),
515                    ]),
516                ),
517            ]),
518        };
519
520        let json_value = fact_to_stable_json(&command).unwrap();
521
522        assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
523        assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
524        assert!(json_value.get("instruction").is_some());
525    }
526
527    #[test]
528    fn test_fact_hash_all_variants() {
529        let test_facts = vec![
530            Fact::Command {
531                id: FactId(1),
532                instruction: JsonValue::empty_object(),
533            },
534            Fact::PayloadUpdate {
535                id: FactId(2),
536                path: "test.path".into(),
537                value: JsonValue::string("test_value"),
538            },
539            Fact::StateTransition {
540                id: FactId(3),
541                cause: FactId(1),
542                new_payload: JsonValue::empty_object(),
543                new_queue: vec![],
544            },
545            Fact::IoRequest {
546                id: FactId(4),
547                cause: FactId(3),
548                io_type: IoType::http_get(),
549                params: JsonValue::empty_object(),
550            },
551            Fact::IoResponse {
552                id: FactId(5),
553                request_id: FactId(4),
554                result: JsonValue::string("response"),
555                error: None,
556            },
557            Fact::IoResponse {
558                id: FactId(6),
559                request_id: FactId(4),
560                result: JsonValue::Null,
561                error: Some("timeout".to_string()),
562            },
563            Fact::Stable {
564                id: FactId(7),
565                version: 0,
566            },
567            Fact::Error {
568                id: FactId(8),
569                message: "test error".into(),
570            },
571        ];
572
573        for fact in test_facts {
574            let hash = fact_hash(&fact).unwrap();
575            assert_eq!(hash.len(), 64);
576
577            let hash2 = fact_hash(&fact).unwrap();
578            assert_eq!(
579                hash,
580                hash2,
581                "fact_hash should be deterministic for {}",
582                fact.type_name()
583            );
584        }
585    }
586
587    #[test]
588    fn test_fact_hash_identity() {
589        let fact1 = Fact::Command {
590            id: FactId(1),
591            instruction: JsonValue::string("same"),
592        };
593        let fact2 = Fact::Command {
594            id: FactId(1),
595            instruction: JsonValue::string("same"),
596        };
597
598        assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
599    }
600
601    #[test]
602    fn test_fact_hash_different_ids() {
603        let fact1 = Fact::Command {
604            id: FactId(1),
605            instruction: JsonValue::string("same"),
606        };
607        let fact2 = Fact::Command {
608            id: FactId(2),
609            instruction: JsonValue::string("same"),
610        };
611
612        assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
613    }
614
615    #[test]
616    fn test_fact_hash_snapshot() {
617        let test_facts = [
618            Fact::Command {
619                id: FactId(1),
620                instruction: JsonValue::empty_object(),
621            },
622            Fact::PayloadUpdate {
623                id: FactId(2),
624                path: "test.path".into(),
625                value: JsonValue::string("test_value"),
626            },
627            Fact::StateTransition {
628                id: FactId(3),
629                cause: FactId(1),
630                new_payload: JsonValue::empty_object(),
631                new_queue: vec![],
632            },
633            Fact::IoRequest {
634                id: FactId(4),
635                cause: FactId(3),
636                io_type: IoType::http_get(),
637                params: JsonValue::empty_object(),
638            },
639            Fact::IoResponse {
640                id: FactId(5),
641                request_id: FactId(4),
642                result: JsonValue::string("response"),
643                error: None,
644            },
645            Fact::Stable {
646                id: FactId(6),
647                version: 0,
648            },
649            Fact::Error {
650                id: FactId(7),
651                message: "test error".into(),
652            },
653        ];
654
655        let current_hashes: Vec<String> =
656            test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
657        let snapshot_file = env!("CARGO_MANIFEST_DIR").to_string() + "/hash_snapshot.txt";
658
659        if std::path::Path::new(&snapshot_file).exists() {
660            let snapshot = std::fs::read_to_string(&snapshot_file).unwrap();
661            let expected_hashes: Vec<String> = snapshot.lines().map(|s| s.to_string()).collect();
662            assert_eq!(
663                current_hashes, expected_hashes,
664                "Hash snapshot mismatch! If this is an expected change (e.g., Fact struct modification), delete {} to regenerate.",
665                snapshot_file
666            );
667        } else {
668            let snapshot_content = current_hashes.join("\n");
669            std::fs::write(&snapshot_file, snapshot_content).unwrap();
670            println!("Created hash snapshot: {}", snapshot_file);
671        }
672    }
673
674    #[test]
675    fn test_compute_chain_hash_empty() {
676        let facts: Vec<Fact> = vec![];
677        let chain_hash = compute_chain_hash(&facts).unwrap();
678        assert_eq!(chain_hash, "genesis");
679    }
680
681    #[test]
682    fn test_compute_chain_hash_deterministic() {
683        let facts = vec![
684            Fact::Command {
685                id: FactId(1),
686                instruction: JsonValue::empty_object(),
687            },
688            Fact::StateTransition {
689                id: FactId(2),
690                cause: FactId(1),
691                new_payload: JsonValue::empty_object(),
692                new_queue: vec![],
693            },
694        ];
695
696        let result1 = compute_chain_hash(&facts).unwrap();
697        let result2 = compute_chain_hash(&facts).unwrap();
698        assert_eq!(result1, result2);
699    }
700
701    #[test]
702    fn test_compute_chain_hash_order_sensitive() {
703        let fact1 = Fact::Command {
704            id: FactId(1),
705            instruction: JsonValue::empty_object(),
706        };
707        let fact2 = Fact::StateTransition {
708            id: FactId(2),
709            cause: FactId(1),
710            new_payload: JsonValue::empty_object(),
711            new_queue: vec![],
712        };
713
714        let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
715        let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
716        assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
717    }
718
719    /// 交叉验证测试:与 evorule-governance/src/hash.rs 的快照比对
720    ///
721    /// 本测试确保 tier1 和 tier2 的哈希算法产生相同的哈希值。
722    /// 如果 tier2 hash.rs 仍维护独立实现,此测试会检测到不一致。
723    #[test]
724    fn test_cross_validate_with_tier2() {
725        // 与 evorule-governance/src/hash.rs 的 test_fact_hash_snapshot 使用相同的 Fact
726        let test_facts = [
727            Fact::Command {
728                id: FactId(1),
729                instruction: JsonValue::empty_object(),
730            },
731            Fact::PayloadUpdate {
732                id: FactId(2),
733                path: "test.path".into(),
734                value: JsonValue::string("test_value"),
735            },
736            Fact::StateTransition {
737                id: FactId(3),
738                cause: FactId(1),
739                new_payload: JsonValue::empty_object(),
740                new_queue: vec![],
741            },
742            Fact::IoRequest {
743                id: FactId(4),
744                cause: FactId(3),
745                io_type: IoType::http_get(),
746                params: JsonValue::empty_object(),
747            },
748            Fact::IoResponse {
749                id: FactId(5),
750                request_id: FactId(4),
751                result: JsonValue::string("response"),
752                error: None,
753            },
754            Fact::Stable {
755                id: FactId(6),
756                version: 0,
757            },
758            Fact::Error {
759                id: FactId(7),
760                message: "test error".into(),
761            },
762        ];
763
764        // 计算 tier1 的哈希
765        let tier1_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
766
767        // 读取 tier2 的快照文件(如果存在)
768        let tier2_snapshot_path = "../../evorule-governance/src/hash_snapshot.txt".to_string();
769        if std::path::Path::new(&tier2_snapshot_path).exists() {
770            let tier2_snapshot = std::fs::read_to_string(&tier2_snapshot_path).unwrap();
771            let tier2_hashes: Vec<String> = tier2_snapshot.lines().map(|s| s.to_string()).collect();
772
773            assert_eq!(
774                tier1_hashes, tier2_hashes,
775                "tier1 和 tier2 的 fact_hash 不一致!\
776                 如果这是预期变更(如 Fact 结构修改),\
777                 请删除 evorule-governance/src/hash_snapshot.txt 重新生成。"
778            );
779        }
780    }
781}