Skip to main content

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