evorule_cli/commands/run.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//! `evorule run` —— 加载并执行 JSON 规则,输出 fact log
5//!
6//! # 流程
7//! 1. `io_util::load_rules`:加载规则目录(确定性排序)
8//! 2. `io_util::parse_initial_payload`:解析初始 payload
9//! 3. 构造初始指令 `{"type":"noop"}` 触发 transform 链
10//! 4. `executor::execute`:同步反应器循环(FIFO + max_steps)
11//! 5. `fact_log::write_facts`:输出 fact log(tier1 WAL 格式)
12//!
13//! # fact log 格式
14//! 与 evorule-reactor WAL 文件格式互换,与 evorule-governance auditor 哈希链互通。
15
16use std::path::Path;
17
18use evorule_tcb::JsonValue;
19
20use crate::error::CliError;
21use crate::{executor, fact_log, io_util};
22
23/// 执行 run 子命令
24///
25/// # 参数
26/// - `rules_dir`:规则目录
27/// - `payload`:初始 payload JSON 字符串(可选)
28/// - `payload_file`:初始 payload 文件(可选,优先级低于 payload)
29/// - `output`:输出文件路径(None 则 stdout)
30/// - `max_steps`:最大执行步数上界
31pub fn run(
32 rules_dir: &Path,
33 payload: Option<&str>,
34 payload_file: Option<&Path>,
35 output: Option<&Path>,
36 max_steps: usize,
37) -> Result<(), CliError> {
38 let transforms = io_util::load_rules(rules_dir)?;
39 let initial_payload = io_util::parse_initial_payload(payload, payload_file)?;
40
41 // 初始指令:noop 触发 transform 链
42 // (core_eval 规则通过 branch domain=instruction_type 匹配 noop 后执行 on_true)
43 let initial_instruction = JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))]);
44
45 tracing::info!(rules = transforms.len(), max_steps, "starting execution");
46
47 // 最终 payload 经返回值直接交付(CR-20260901-001:Stable 不再内嵌快照)
48 let (facts, _final_payload) =
49 executor::execute(&transforms, initial_payload, initial_instruction, max_steps)?;
50
51 // 输出 fact log(tier1 WAL 格式)
52 fact_log::write_facts(output, &facts)?;
53
54 // stderr 摘要(不影响 fact log 输出)
55 // CR-20260902-001(UV-046 C1/C3):Error fact 不再静默成功——返回
56 // ExecutionHadErrors → 退出码 3,CI/自动化管道可正确感知规则执行失败。
57 // fact log 已写出,供审计回放定位失败原因。
58 let error_count = facts
59 .iter()
60 .filter(|f| matches!(f, evorule_reactor::Fact::Error { .. }))
61 .count();
62 if error_count > 0 {
63 tracing::warn!(facts = facts.len(), error_count, "execution completed with Error facts");
64 return Err(CliError::ExecutionHadErrors { count: error_count });
65 }
66 tracing::info!(facts = facts.len(), "execution completed successfully");
67
68 Ok(())
69}