use std::fmt;
use evorule_tcb::JsonValue;
use tracing::{debug, trace};
use crate::fact::Fact;
#[derive(Debug)]
pub struct HashError {
message: String,
}
impl HashError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for HashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "哈希计算错误: {}", self.message)
}
}
impl std::error::Error for HashError {}
fn tcb_to_serde(value: &JsonValue) -> serde_json::Value {
match value {
JsonValue::Null => serde_json::Value::Null,
JsonValue::Bool(b) => serde_json::Value::Bool(*b),
JsonValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
JsonValue::String(s) => serde_json::Value::String(s.to_string()),
JsonValue::Array(arr) => serde_json::Value::Array(arr.iter().map(tcb_to_serde).collect()),
JsonValue::Object(obj) => {
let mut map = serde_json::Map::new();
for (k, v) in obj.iter() {
map.insert(k.clone(), tcb_to_serde(v));
}
serde_json::Value::Object(map)
}
}
}
fn trace_attr_to_json(attr: &crate::fact::TraceDomainAttribution) -> serde_json::Value {
let mut attr_obj = serde_json::Map::new();
attr_obj.insert(
"outcome".into(),
serde_json::Value::String(attr.outcome.clone()),
);
if let Some(reason) = &attr.missing_reason {
attr_obj.insert(
"missing_reason".into(),
serde_json::Value::String(reason.clone()),
);
}
if let Some(policy) = &attr.on_missing {
attr_obj.insert(
"on_missing".into(),
serde_json::Value::String(policy.clone()),
);
}
serde_json::Value::Object(attr_obj)
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
pub fn fact_to_stable_json(fact: &Fact) -> Result<serde_json::Value, HashError> {
let fact_type = fact.type_name();
let fact_id = fact.id();
trace!(
事实ID = ?fact_id,
事实类型 = %fact_type,
"序列化开始"
);
let mut obj = serde_json::Map::new();
match fact {
Fact::Command { id, instruction } => {
trace!(
事实ID = ?id,
指令大小 = instruction.to_string().len(),
"处理命令类型事实"
);
obj.insert("type".into(), serde_json::Value::String("Command".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("instruction".into(), tcb_to_serde(instruction));
}
Fact::PayloadUpdate { id, path, value } => {
trace!(
事实ID = ?id,
路径 = %path,
"处理载荷更新类型事实"
);
obj.insert(
"type".into(),
serde_json::Value::String("PayloadUpdate".into()),
);
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("path".into(), serde_json::Value::String(path.clone()));
obj.insert("value".into(), tcb_to_serde(value));
}
Fact::StateTransition {
id,
cause,
new_payload,
new_queue,
} => {
trace!(
事实ID = ?id,
原因ID = ?cause,
队列长度 = new_queue.len(),
"处理状态转换类型事实"
);
obj.insert(
"type".into(),
serde_json::Value::String("StateTransition".into()),
);
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
obj.insert("new_payload".into(), tcb_to_serde(new_payload));
obj.insert(
"new_queue".into(),
serde_json::Value::Array(new_queue.iter().map(tcb_to_serde).collect()),
);
}
Fact::IoRequest {
id,
cause,
io_type,
params,
} => {
trace!(
事实ID = ?id,
原因ID = ?cause,
IO类型 = %io_type.as_str(),
"处理IO请求类型事实"
);
obj.insert("type".into(), serde_json::Value::String("IoRequest".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
obj.insert(
"io_type".into(),
serde_json::Value::String(io_type.as_str().into()),
);
obj.insert("params".into(), tcb_to_serde(params));
}
Fact::IoResponse {
id,
request_id,
result,
error,
} => {
trace!(
事实ID = ?id,
请求ID = ?request_id,
是否有错误 = error.is_some(),
"处理IO响应类型事实"
);
obj.insert(
"type".into(),
serde_json::Value::String("IoResponse".into()),
);
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert(
"request_id".into(),
serde_json::Value::Number(request_id.0.into()),
);
obj.insert("result".into(), tcb_to_serde(result));
obj.insert(
"error".into(),
error
.as_ref()
.map(|e| serde_json::Value::String(e.clone()))
.unwrap_or(serde_json::Value::Null),
);
}
Fact::Stable { id, version } => {
trace!(事实ID = ?id, 版本 = version, "处理稳定状态类型事实");
obj.insert("type".into(), serde_json::Value::String("Stable".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert(
"version".into(),
serde_json::Value::Number((*version).into()),
);
}
Fact::Error { id, message } => {
trace!(
事实ID = ?id,
消息 = %message,
"处理错误类型事实"
);
obj.insert("type".into(), serde_json::Value::String("Error".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("message".into(), serde_json::Value::String(message.clone()));
}
Fact::TransitionTrace {
id,
cause,
rule_hits,
} => {
trace!(事实ID = ?id, 原因ID = ?cause, 命中数 = rule_hits.len(), "处理命中归因轨迹事实");
obj.insert(
"type".into(),
serde_json::Value::String("TransitionTrace".into()),
);
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
let hits: Vec<serde_json::Value> = rule_hits
.iter()
.map(|h| {
let mut hit_obj = serde_json::Map::new();
hit_obj.insert("index".into(), serde_json::json!(h.index));
hit_obj.insert(
"instr_type".into(),
serde_json::Value::String(h.instr_type.clone()),
);
hit_obj.insert("hit".into(), serde_json::Value::Bool(h.hit));
if let Some(attr) = &h.domain_attr {
hit_obj.insert("domain_attr".into(), trace_attr_to_json(attr));
}
serde_json::Value::Object(hit_obj)
})
.collect();
obj.insert("rule_hits".into(), serde_json::Value::Array(hits));
}
Fact::Violation {
id,
cause,
rule_index,
reason,
instruction,
} => {
trace!(事实ID = ?id, 原因ID = ?cause, 规则下标 = rule_index, "处理违规拦截事实");
obj.insert("type".into(), serde_json::Value::String("Violation".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
obj.insert(
"rule_index".into(),
serde_json::Value::Number((*rule_index).into()),
);
obj.insert("reason".into(), serde_json::Value::String(reason.clone()));
obj.insert("instruction".into(), tcb_to_serde(instruction));
}
}
let value = serde_json::Value::Object(obj);
trace!(
事实ID = ?fact_id,
事实类型 = %fact_type,
"序列化完成"
);
Ok(value)
}
#[allow(dead_code)] pub fn content_hash(value: &JsonValue) -> Result<String, HashError> {
#[cfg(kani)]
{
Ok(String::from("c"))
}
#[cfg(not(kani))]
{
let serde_value = tcb_to_serde(value);
let serialized = serde_json::to_string(&serde_value)
.map_err(|e| HashError::new(format!("内容哈希序列化失败: {}", e)))?;
let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
trace!(
序列化长度 = serialized.len(),
哈希值 = %hash,
"内容哈希计算完成"
);
Ok(hash)
}
}
pub fn fact_hash(fact: &Fact) -> Result<String, HashError> {
#[cfg(kani)]
{
let id = fact.id().0;
let h = match id % 7 {
0 => "a",
1 => "b",
2 => "c",
3 => "d",
4 => "e",
5 => "f",
_ => "g",
};
Ok(String::from(h))
}
#[cfg(not(kani))]
{
let fact_type = fact.type_name();
let fact_id = fact.id();
debug!(
事实ID = ?fact_id,
事实类型 = %fact_type,
"开始计算事实哈希"
);
let value = fact_to_stable_json(fact)?;
let serialized = serde_json::to_string(&value)
.map_err(|e| HashError::new(format!("序列化失败: {}", e)))?;
let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
debug!(
事实ID = ?fact_id,
事实类型 = %fact_type,
哈希值 = %hash,
序列化长度 = serialized.len(),
"事实哈希计算完成"
);
Ok(hash)
}
}
pub fn compute_chain_hash(facts: &[Fact]) -> Result<String, HashError> {
let fact_count = facts.len();
debug!(事实数量 = fact_count, "开始计算链哈希");
if fact_count == 0 {
debug!("事实列表为空,返回 genesis 哈希");
return Ok(String::from("genesis"));
}
let mut prev_hash = String::from("genesis");
trace!(
初始前序哈希 = %prev_hash,
"初始化前序哈希为创世值"
);
for (index, fact) in facts.iter().enumerate() {
let fact_type = fact.type_name();
let fact_id = fact.id();
trace!(
索引 = index,
事实ID = ?fact_id,
事实类型 = %fact_type,
前序哈希 = %prev_hash,
"处理事实"
);
let fh = fact_hash(fact)?;
trace!(
索引 = index,
事实ID = ?fact_id,
事实哈希 = %fh,
"事实哈希计算完成"
);
let current = chain_step(&prev_hash, &fh);
trace!(
索引 = index,
事实ID = ?fact_id,
当前哈希 = %current,
"计算当前哈希(前序哈希+事实哈希)"
);
prev_hash = current;
}
debug!(
事实数量 = fact_count,
最终哈希 = %prev_hash,
"链哈希计算完成"
);
Ok(prev_hash)
}
pub fn chain_step(prev_hash: &str, content_hash: &str) -> String {
#[cfg(kani)]
{
let mut s = String::from(prev_hash);
s.push_str(content_hash);
s
}
#[cfg(not(kani))]
{
let combined = format!("{}{}", prev_hash, content_hash);
blake3::hash(combined.as_bytes()).to_hex().to_string()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
use super::*;
use crate::fact::{Fact, FactId, IoType, TraceDomainAttribution, TraceHit};
use evorule_tcb::JsonValue;
#[test]
fn test_fact_to_stable_json_format() {
let command = Fact::Command {
id: FactId(1),
instruction: JsonValue::object_from_pairs(&[
("type", JsonValue::string("increment")),
(
"params",
JsonValue::object_from_pairs(&[
("attr", JsonValue::string("x")),
("delta", JsonValue::Integer(5)),
]),
),
]),
};
let json_value = fact_to_stable_json(&command).unwrap();
assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
assert!(json_value.get("instruction").is_some());
}
#[test]
fn test_transition_trace_hash_without_attr_matches_legacy_shape() {
let fact = Fact::TransitionTrace {
id: FactId(9),
cause: FactId(2),
rule_hits: vec![TraceHit {
index: 0,
instr_type: "branch".into(),
hit: false,
domain_attr: None,
}],
};
let json = fact_to_stable_json(&fact).unwrap();
let hits = json.get("rule_hits").unwrap().as_array().unwrap();
assert_eq!(hits.len(), 1);
let hit = hits.first().unwrap();
assert_eq!(
hit,
&serde_json::json!({"index": 0, "instr_type": "branch", "hit": false}),
"无归因条目必须与旧格式逐键一致(None 省略,存量哈希不变)"
);
}
#[test]
fn test_transition_trace_hash_with_attr_shape_and_distinct() {
let fact_with = Fact::TransitionTrace {
id: FactId(9),
cause: FactId(2),
rule_hits: vec![TraceHit {
index: 0,
instr_type: "branch".into(),
hit: false,
domain_attr: Some(TraceDomainAttribution {
outcome: "missing".into(),
missing_reason: Some("path_not_found".into()),
on_missing: Some("unsat".into()),
}),
}],
};
let fact_without = Fact::TransitionTrace {
id: FactId(9),
cause: FactId(2),
rule_hits: vec![TraceHit {
index: 0,
instr_type: "branch".into(),
hit: false,
domain_attr: None,
}],
};
let json = fact_to_stable_json(&fact_with).unwrap();
let hit = json
.get("rule_hits")
.unwrap()
.as_array()
.unwrap()
.first()
.unwrap();
assert_eq!(
hit.get("domain_attr").unwrap(),
&serde_json::json!({
"outcome": "missing",
"missing_reason": "path_not_found",
"on_missing": "unsat"
}),
"归因条目形状(None 字段省略)"
);
let hash_with = fact_hash(&fact_with).unwrap();
let hash_without = fact_hash(&fact_without).unwrap();
assert_ne!(
hash_with, hash_without,
"归因事实必须纳入哈希链(有归因 vs 无归因哈希不同)"
);
}
#[test]
fn test_fact_hash_all_variants() {
let test_facts = vec![
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::PayloadUpdate {
id: FactId(2),
path: "test.path".into(),
value: JsonValue::string("test_value"),
},
Fact::StateTransition {
id: FactId(3),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
Fact::IoRequest {
id: FactId(4),
cause: FactId(3),
io_type: IoType::http_get(),
params: JsonValue::empty_object(),
},
Fact::IoResponse {
id: FactId(5),
request_id: FactId(4),
result: JsonValue::string("response"),
error: None,
},
Fact::IoResponse {
id: FactId(6),
request_id: FactId(4),
result: JsonValue::Null,
error: Some("timeout".to_string()),
},
Fact::Stable {
id: FactId(7),
version: 0,
},
Fact::Error {
id: FactId(8),
message: "test error".into(),
},
];
for fact in test_facts {
let hash = fact_hash(&fact).unwrap();
assert_eq!(hash.len(), 64);
let hash2 = fact_hash(&fact).unwrap();
assert_eq!(
hash,
hash2,
"fact_hash should be deterministic for {}",
fact.type_name()
);
}
}
#[test]
fn test_fact_hash_identity() {
let fact1 = Fact::Command {
id: FactId(1),
instruction: JsonValue::string("same"),
};
let fact2 = Fact::Command {
id: FactId(1),
instruction: JsonValue::string("same"),
};
assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
}
#[test]
fn test_fact_hash_different_ids() {
let fact1 = Fact::Command {
id: FactId(1),
instruction: JsonValue::string("same"),
};
let fact2 = Fact::Command {
id: FactId(2),
instruction: JsonValue::string("same"),
};
assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
}
#[test]
fn test_fact_hash_snapshot() {
let test_facts = [
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::PayloadUpdate {
id: FactId(2),
path: "test.path".into(),
value: JsonValue::string("test_value"),
},
Fact::StateTransition {
id: FactId(3),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
Fact::IoRequest {
id: FactId(4),
cause: FactId(3),
io_type: IoType::http_get(),
params: JsonValue::empty_object(),
},
Fact::IoResponse {
id: FactId(5),
request_id: FactId(4),
result: JsonValue::string("response"),
error: None,
},
Fact::Stable {
id: FactId(6),
version: 0,
},
Fact::Error {
id: FactId(7),
message: "test error".into(),
},
];
let current_hashes: Vec<String> =
test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
let snapshot_file = env!("CARGO_MANIFEST_DIR").to_string() + "/hash_snapshot.txt";
if std::path::Path::new(&snapshot_file).exists() {
let snapshot = std::fs::read_to_string(&snapshot_file).unwrap();
let expected_hashes: Vec<String> = snapshot.lines().map(|s| s.to_string()).collect();
assert_eq!(
current_hashes, expected_hashes,
"Hash snapshot mismatch! If this is an expected change (e.g., Fact struct modification), delete {} to regenerate.",
snapshot_file
);
} else {
let snapshot_content = current_hashes.join("\n");
std::fs::write(&snapshot_file, snapshot_content).unwrap();
println!("Created hash snapshot: {}", snapshot_file);
}
}
#[test]
fn test_compute_chain_hash_empty() {
let facts: Vec<Fact> = vec![];
let chain_hash = compute_chain_hash(&facts).unwrap();
assert_eq!(chain_hash, "genesis");
}
#[test]
fn test_compute_chain_hash_deterministic() {
let facts = vec![
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::StateTransition {
id: FactId(2),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
];
let result1 = compute_chain_hash(&facts).unwrap();
let result2 = compute_chain_hash(&facts).unwrap();
assert_eq!(result1, result2);
}
#[test]
fn test_compute_chain_hash_order_sensitive() {
let fact1 = Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
};
let fact2 = Fact::StateTransition {
id: FactId(2),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
};
let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
}
#[test]
fn test_cross_validate_with_tier2() {
let test_facts = [
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::PayloadUpdate {
id: FactId(2),
path: "test.path".into(),
value: JsonValue::string("test_value"),
},
Fact::StateTransition {
id: FactId(3),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
Fact::IoRequest {
id: FactId(4),
cause: FactId(3),
io_type: IoType::http_get(),
params: JsonValue::empty_object(),
},
Fact::IoResponse {
id: FactId(5),
request_id: FactId(4),
result: JsonValue::string("response"),
error: None,
},
Fact::Stable {
id: FactId(6),
version: 0,
},
Fact::Error {
id: FactId(7),
message: "test error".into(),
},
];
let tier1_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
let tier2_snapshot_path = "../../evorule-governance/src/hash_snapshot.txt".to_string();
if std::path::Path::new(&tier2_snapshot_path).exists() {
let tier2_snapshot = std::fs::read_to_string(&tier2_snapshot_path).unwrap();
let tier2_hashes: Vec<String> = tier2_snapshot.lines().map(|s| s.to_string()).collect();
assert_eq!(
tier1_hashes, tier2_hashes,
"tier1 和 tier2 的 fact_hash 不一致!\
如果这是预期变更(如 Fact 结构修改),\
请删除 evorule-governance/src/hash_snapshot.txt 重新生成。"
);
}
}
}