Skip to main content

evorule_cli/
executor.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//! 同步反应器循环 —— CLI 执行核心
5//!
6//! # P0 修复
7//! 1. **FIFO 队列**:用 `VecDeque::pop_front()`,不能用 `Vec::pop()`。
8//!    tier0 `exec_push` 用 `new_queue.append(queue)` 把新指令前置(插队语义),
9//!    必须从前端取才能保证 push 的指令先执行。
10//! 2. **max_steps 上界**:先检后 pop(对齐 evorule-reactor reactor.rs BUG-3 修复)。
11//!    默认 10000,可 `--max-steps` 覆盖。超限发 `Fact::Error` + break。
12//! 3. **I/O 两阶段架构**:`pending_io: HashMap<FactId, JsonValue>` 缓存 orig 指令。
13//!    0.2.0 无 handler 时发 `Fact::Error` 退出,但架构正确——0.3.0 加 handler 时
14//!    只需在 IoRequest 分支注入 IoResponse + push_front(orig) 即可,循环主体不变。
15//!
16//! # 不引入 tokio runtime
17//! `execute_transition` 是同步纯函数,整个循环无 await。tokio 仅作为 evorule-reactor
18//! 编译依赖存在,CLI 不创建 runtime。
19//!
20//! # Fact 序列
21//! 执行产生 `Vec<Fact>`:
22//! 1. `Command`(初始指令)
23//! 2. 若干 `StateTransition`(每步执行)
24//! 3. 可选 `IoRequest` + `Error`(I/O 请求但无 handler)
25//! 4. 可选 `Error`(TCB 错误或 max_steps 超限)
26//! 5. `Stable`(稳定标记,始终发射)
27//!
28//! 最终 payload 经返回值直接交给调用方(:Stable 不再
29//! 内嵌全量快照,状态本体从执行器持有的 payload 返回,不经事实链)。
30
31use std::collections::{HashMap, VecDeque};
32
33use evorule_reactor::{Fact, FactId, FactIdGenerator, IoType};
34use evorule_tcb::{execute_transition, JsonValue, TransitionResult};
35
36use crate::error::CliError;
37
38/// 默认 max_steps(与 evorule-reactor 默认 max_rounds 一致量级)
39pub const DEFAULT_MAX_STEPS: usize = 10000;
40
41/// 执行规则,产生 Fact 序列
42///
43/// # 参数
44/// - `core_eval`:transform 规则列表(由 `io_util::load_rules` 加载)
45/// - `initial_payload`:初始 payload
46/// - `initial_instruction`:初始指令(通常是 `{"type":"noop"}` 触发 transform 链)
47/// - `max_steps`:最大执行步数上界(先检后 pop)
48///
49/// # 返回
50/// `(Vec<Fact>, JsonValue)`:fact 序列(Command、若干 StateTransition、
51/// 可选 Error、结尾 Stable)+ 最终 payload(执行器持有,直接返回)
52///
53/// # 不变量
54/// - FIFO 队列:`VecDeque::pop_front`,不能用 `Vec::pop`
55/// - max_steps 先检后 pop:超限发 Error + break
56/// - I/O 两阶段:IoRequest 时缓存 orig 指令到 pending_io,0.2.0 无 handler 发 Error
57// 108 行: CLI 主循环 + I/O 两阶段 + max_steps 门禁 + 错误处理必须单函数原子语义
58// 拆函数会让 4 阶段 (loop / dispatch / pending_io / break) 状态传递出错
59#[allow(clippy::too_many_lines)]
60pub fn execute(
61    core_eval: &[JsonValue],
62    initial_payload: JsonValue,
63    initial_instruction: JsonValue,
64    max_steps: usize,
65) -> Result<(Vec<Fact>, JsonValue), CliError> {
66    let mut facts: Vec<Fact> = Vec::new();
67    let mut id_gen = FactIdGenerator::new();
68    let mut queue: VecDeque<JsonValue> = VecDeque::new();
69    queue.push_back(initial_instruction);
70    let mut payload = initial_payload;
71    let mut steps = 0;
72    // 会话版本号:对齐 reactor 语义,每条 StateTransition +1
73    let mut version: u64 = 0;
74    // 0.2.0 无 I/O handler,pending_io 仅缓存不消费(为 0.3.0 铺路)
75    let mut pending_io: HashMap<FactId, JsonValue> = HashMap::new();
76
77    // 发射初始 Command fact
78    let cmd_id = id_gen.next_id();
79    let mut current_cause: FactId = cmd_id;
80    let cmd_instruction = queue.front().cloned().unwrap_or(JsonValue::Null);
81    facts.push(Fact::Command {
82        id: cmd_id,
83        instruction: cmd_instruction,
84    });
85
86    while !queue.is_empty() {
87        // max_steps 先检后 pop(对齐 evorule-reactor BUG-3 修复)
88        if steps >= max_steps {
89            let err_id = id_gen.next_id();
90            facts.push(Fact::Error {
91                id: err_id,
92                message: format!("max_steps exceeded: {}", steps),
93            });
94            tracing::warn!(steps, max_steps, "max_steps exceeded");
95            break;
96        }
97
98        // FIFO:pop_front(不能用 Vec::pop,那是 LIFO)
99        let instruction = match queue.pop_front() {
100            Some(i) => i,
101            None => break, // 逻辑不可达(while 条件已检查),防御性
102        };
103        steps += 1;
104
105        // 传当前 queue 快照给 execute_transition(供 core_eval 规则引用 __exec__.queue)
106        let queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
107        let result = execute_transition(core_eval, &instruction, &payload, &queue_snapshot);
108
109        match result {
110            Ok(TransitionResult::State {
111                new_payload,
112                new_queue,
113                // cli 为校验工具,不采集命中归因(运行时归因在 server 链路)
114                rule_hits: _,
115            }) => {
116                payload = new_payload;
117                queue = new_queue.into_iter().collect();
118                version += 1;
119                let id = id_gen.next_id();
120                let new_queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
121                facts.push(Fact::StateTransition {
122                    id,
123                    cause: current_cause,
124                    new_payload: payload.clone(),
125                    new_queue: new_queue_snapshot,
126                });
127                current_cause = id;
128            }
129            Ok(TransitionResult::IoRequired { io_type, params }) => {
130                // v0.2.0:io_type 透传不校验(parse 已 deprecated,无条件接受)
131                let io_type = IoType::new(&io_type);
132                let req_id = id_gen.next_id();
133                // 缓存 orig 指令(0.3.0 加 handler 时用于 push_front 重执行)
134                pending_io.insert(req_id, instruction.clone());
135                facts.push(Fact::IoRequest {
136                    id: req_id,
137                    cause: current_cause,
138                    io_type: io_type.clone(),
139                    params,
140                });
141                // 0.2.0 无 I/O handler,发 Error 退出
142                let err_id = id_gen.next_id();
143                facts.push(Fact::Error {
144                    id: err_id,
145                    message: format!("no I/O handler for io_type={}", io_type.as_str()),
146                });
147                tracing::warn!(
148                    io_type = %io_type.as_str(),
149                    request_id = ?req_id,
150                    "I/O required but no handler available, stopping"
151                );
152                break;
153            }
154            Ok(TransitionResult::Ignored {
155                instruction_type,
156                reason,
157                // cli 为校验工具,不采集命中归因(运行时归因在 server 链路)
158                rule_hits: _,
159            }) => {
160                // v0.3.1:指令被静默忽略(无匹配 transform 规则或规则产生 noop 效果)。
161                // 与 reactor 行为一致:产生 Error 事实使系统显式感知此问题
162                let err_id = id_gen.next_id();
163                let msg = format!(
164                    "Instruction ignored by TCB: type={}, reason={}, instruction={:?}",
165                    instruction_type, reason, instruction
166                );
167                facts.push(Fact::Error {
168                    id: err_id,
169                    message: msg,
170                });
171                tracing::warn!(
172                    instruction_type = %instruction_type,
173                    reason = %reason,
174                    "TCB silently ignored instruction (no matching rule or noop effect)"
175                );
176                break;
177            }
178            Ok(TransitionResult::Halted { rule_index, reason }) => {
179                // 回归验证:enforce 强制原语命中——违规指令被拒绝执行(不执行、不回队)。
180                // CLI 与 reactor 同语义:系统独占发射 Violation 事实,继续处理后续指令
181                let v_id = id_gen.next_id();
182                facts.push(Fact::Violation {
183                    id: v_id,
184                    cause: current_cause,
185                    rule_index: rule_index as u64,
186                    reason: reason.clone(),
187                    instruction: instruction.clone(),
188                });
189                tracing::warn!(
190                    rule_index,
191                    %reason,
192                    "enforce primitive blocked: violating instruction rejected"
193                );
194                continue;
195            }
196            Err(e) => {
197                let err_id = id_gen.next_id();
198                let msg = format!("TCB error at step {}: {}", steps, e);
199                facts.push(Fact::Error {
200                    id: err_id,
201                    message: msg,
202                });
203                tracing::error!(step = steps, error = %e, "TCB execution error");
204                break;
205            }
206        }
207    }
208
209    // 始终发射 Stable(即使是 Error 退出,也标记当前版本稳定)。
210    // 最终 payload 经返回值直接交付(:不再内嵌快照)
211    let stable_id = id_gen.next_id();
212    facts.push(Fact::Stable {
213        id: stable_id,
214        version,
215    });
216
217    Ok((facts, payload))
218}
219
220#[cfg(test)]
221mod tests {
222    #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
223    use super::*;
224    use evorule_tcb::JsonValue;
225
226    /// 构造 noop 指令
227    fn noop_instruction() -> JsonValue {
228        JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))])
229    }
230
231    /// 构造无条件 push 规则:push 一条 noop 到队列前端
232    fn push_noop_rule() -> JsonValue {
233        JsonValue::object_from_pairs(&[
234            ("type", JsonValue::string("push")),
235            (
236                "params",
237                JsonValue::object_from_pairs(&[(
238                    "instructions",
239                    JsonValue::array(vec![noop_instruction()]),
240                )]),
241            ),
242        ])
243    }
244
245    /// 构造无条件 io_request 规则
246    fn io_request_rule(io_type: &str) -> JsonValue {
247        JsonValue::object_from_pairs(&[
248            ("type", JsonValue::string("io_request")),
249            (
250                "params",
251                JsonValue::object_from_pairs(&[
252                    ("io_type", JsonValue::string(io_type)),
253                    ("url", JsonValue::string("http://example.com")),
254                ]),
255            ),
256        ])
257    }
258
259    #[test]
260    fn test_execute_empty_core_eval_noop() {
261        // v0.3.1:空 core_eval + noop 指令 → TCB 显式返回 `Ignored`(无匹配 transform 规则)
262        // cli executor 按 reactor 一致行为产生 `Error` 事实(不再静默失败)
263        let (facts, _) = execute(
264            &[],
265            JsonValue::empty_object(),
266            noop_instruction(),
267            DEFAULT_MAX_STEPS,
268        )
269        .unwrap();
270
271        // 应产生:Command + Error(ignored by TCB) + Stable
272        assert_eq!(facts.len(), 3, "expected Command + Error(ignored) + Stable");
273        assert!(matches!(facts[0], Fact::Command { .. }));
274        match &facts[1] {
275            Fact::Error { message, .. } => {
276                assert!(
277                    message.contains("ignored by TCB"),
278                    "expected TCB ignored error, got: {}",
279                    message
280                );
281            }
282            other => panic!("expected Error for Ignored, got {:?}", other),
283        }
284        assert!(matches!(facts[2], Fact::Stable { .. }));
285    }
286
287    #[test]
288    fn test_execute_max_steps_zero() {
289        // max_steps=0:立即发 Error,不执行任何指令
290        let (facts, _) = execute(&[], JsonValue::empty_object(), noop_instruction(), 0).unwrap();
291
292        // 应产生:Command + Error + Stable
293        assert_eq!(facts.len(), 3, "expected Command + Error + Stable");
294        assert!(matches!(facts[0], Fact::Command { .. }));
295        match &facts[1] {
296            Fact::Error { message, .. } => {
297                assert!(message.contains("max_steps"), "message: {}", message);
298            }
299            other => panic!("expected Error, got {:?}", other),
300        }
301        assert!(matches!(facts[2], Fact::Stable { .. }));
302    }
303
304    #[test]
305    fn test_execute_max_steps_exceeded_with_push() {
306        // core_eval 含 push 规则(无限循环),max_steps=3 限制
307        let core_eval = vec![push_noop_rule()];
308        let (facts, _) =
309            execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
310
311        // 应产生:Command + 3×StateTransition + Error + Stable = 6
312        // (steps 0,1,2 各产生 StateTransition,step 3 触发 max_steps)
313        assert!(
314            facts.len() >= 4,
315            "expected at least Command + StateTransitions + Error + Stable, got {}",
316            facts.len()
317        );
318        // 最后第二个应为 Error
319        let last_idx = facts.len() - 2;
320        match &facts[last_idx] {
321            Fact::Error { message, .. } => {
322                assert!(message.contains("max_steps"), "message: {}", message);
323            }
324            other => panic!("expected Error at index {}, got {:?}", last_idx, other),
325        }
326        // 最后应为 Stable
327        assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
328    }
329
330    #[test]
331    fn test_execute_push_produces_nonempty_queue() {
332        // core_eval 含 push 规则,max_steps=1 只执行一步
333        let core_eval = vec![push_noop_rule()];
334        let (facts, _) =
335            execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 1).unwrap();
336
337        // 第一个 StateTransition 的 new_queue 应非空(push 生效)
338        let st = facts.iter().find_map(|f| {
339            if let Fact::StateTransition { new_queue, .. } = f {
340                Some(new_queue)
341            } else {
342                None
343            }
344        });
345        let new_queue = st.expect("should have StateTransition");
346        assert!(
347            !new_queue.is_empty(),
348            "push rule should produce non-empty new_queue"
349        );
350    }
351
352    #[test]
353    fn test_execute_io_request_produces_io_fact() {
354        // core_eval 含 io_request 规则
355        let core_eval = vec![io_request_rule("call_external")];
356        let (facts, _) = execute(
357            &core_eval,
358            JsonValue::empty_object(),
359            noop_instruction(),
360            DEFAULT_MAX_STEPS,
361        )
362        .unwrap();
363
364        // 应产生:Command + IoRequest + Error + Stable
365        let has_io_request = facts.iter().any(|f| matches!(f, Fact::IoRequest { .. }));
366        assert!(has_io_request, "should have IoRequest fact");
367
368        let has_error = facts.iter().any(
369            |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
370        );
371        assert!(has_error, "should have Error fact about no I/O handler");
372
373        // 最后应为 Stable
374        assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
375    }
376
377    #[test]
378    fn test_execute_unknown_io_type_produces_error() {
379        // v0.2.0:io_type 透传不校验,"unknown_io_type" 不再被 parse 拒绝,
380        // 而是透传后由 cli(无 handler)发 "no I/O handler for io_type=unknown_io_type" Error
381        let core_eval = vec![io_request_rule("unknown_io_type")];
382        let (facts, _) = execute(
383            &core_eval,
384            JsonValue::empty_object(),
385            noop_instruction(),
386            DEFAULT_MAX_STEPS,
387        )
388        .unwrap();
389
390        let has_error = facts.iter().any(
391            |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
392        );
393        assert!(
394            has_error,
395            "should have Error (no I/O handler) for unknown io_type"
396        );
397    }
398
399    #[test]
400    fn test_execute_tcb_error_produces_error() {
401        // 构造会触发 TCB 错误的场景:core_eval 含非法规则(缺 params)
402        let bad_rule = JsonValue::object_from_pairs(&[("type", JsonValue::string("set"))]);
403        let (facts, _) = execute(
404            &[bad_rule],
405            JsonValue::empty_object(),
406            noop_instruction(),
407            DEFAULT_MAX_STEPS,
408        )
409        .unwrap();
410
411        // 应产生 Error(TCB error)
412        let has_error = facts
413            .iter()
414            .any(|f| matches!(f, Fact::Error { ref message, .. } if message.contains("TCB error")));
415        assert!(has_error, "should have TCB error fact");
416    }
417
418    #[test]
419    fn test_execute_fact_ids_monotonic() {
420        // 验证 FactId 单调递增
421        let (facts, _) = execute(
422            &[],
423            JsonValue::empty_object(),
424            noop_instruction(),
425            DEFAULT_MAX_STEPS,
426        )
427        .unwrap();
428
429        let ids: Vec<u64> = facts.iter().map(|f| f.id().0).collect();
430        for i in 1..ids.len() {
431            assert!(
432                ids[i] > ids[i - 1],
433                "FactIds must be monotonically increasing: {:?}",
434                ids
435            );
436        }
437    }
438
439    #[test]
440    fn test_execute_fifo_pop_front_semantics() {
441        // 验证 FIFO:push 两条指令后,按顺序执行
442        // core_eval: push [noop, noop](两条指令)
443        let push_two = JsonValue::object_from_pairs(&[
444            ("type", JsonValue::string("push")),
445            (
446                "params",
447                JsonValue::object_from_pairs(&[(
448                    "instructions",
449                    JsonValue::array(vec![noop_instruction(), noop_instruction()]),
450                )]),
451            ),
452        ]);
453        let core_eval = vec![push_two];
454        // max_steps=3:初始指令执行 push(step1),然后执行 push 的两条 noop(step2,3)
455        let (facts, _) =
456            execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
457
458        // 验证:每次执行都通过 pop_front 取指令(FIFO)
459        // step1: pop_front 初始 noop → push [noop, noop] → queue=[noop, noop]
460        // step2: pop_front noop(第1条) → push [noop, noop] → queue=[noop, noop, noop]
461        // step3: pop_front noop → push → queue=[noop, noop, noop]
462        // max_steps=3 触发 Error
463        let state_transitions: Vec<_> = facts
464            .iter()
465            .filter(|f| matches!(f, Fact::StateTransition { .. }))
466            .collect();
467        assert!(
468            !state_transitions.is_empty(),
469            "should have at least one StateTransition"
470        );
471        // 第一个 StateTransition 的 new_queue 应有 2 条指令(push 的结果)
472        if let Fact::StateTransition { new_queue, .. } = state_transitions[0] {
473            assert_eq!(
474                new_queue.len(),
475                2,
476                "first push should produce 2 instructions in queue"
477            );
478        }
479    }
480}