Skip to main content

evorule_cli/commands/
validate.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 validate` —— 规则集形态门禁(L2 纪律)
5//!
6//! # 从白名单到门禁
7//!
8//! 本命令最初只做一件事:校验顶层 `type` 是否在 core_eval 元指令白名单
9//! (`META_INSTRUCTION_TYPES`)内。那份「规范」被写死在 Rust 里,只能回答一个
10//! 问题——**顶层类型认不认识**。
11//!
12//! BUG-P0-005 暴露了它回答不了的问题:每条规则的 `type` 都合法、整个规则集
13//! 仍然可能让守卫**永不求值**(L1 `io_request` 排在 L2 `enforce` 之前、命中即
14//! 返回,后者永远求不到值)。这不是「某条规则写错」,而是**排列与组合不合法**
15//! ——性质上属于规则集的形态,不属于单条规则。
16//!
17//! 故本命令改为形态门禁:
18//! - **判定条款**在 `evorule-tcb/discipline/core_eval.json`(数据:可版本化、
19//!   可 diff、可评审,随内核发版,不存在「规范与代码各写一份」的漂移);
20//! - **遍历与事实标注**在 `discipline_gate`(机制:把内核看不见的列表级事实
21//!   注入 `_ctx`,判定全部交给 `execute_transition` 求值)。
22//!
23//! 作用域从「顶层 N 条」扩大到「含 branch 内嵌子指令的全部节点」——
24//! BUG 不住在顶层。
25//!
26//! # 退出码
27//! - 0:通过门禁
28//! - 1:有纪律违规(含未知元指令 —— 即历史上的 DC-01 前身)
29//!
30//! # 行为变更提示
31//!
32//! 老版本只认顶层 type;新版本会额外拦截会让守卫失效的形态(DC-05 遮蔽)、
33//! 破坏重放等价性的形态(DC-08/DC-09)等。**首次在新仓库跑出现违规属预期**,
34//! 这些违规历史上一直存在,只是此前没有任何东西检查它们。
35
36use std::path::Path;
37
38use crate::error::CliError;
39use crate::io_util;
40
41/// 执行 validate 子命令
42pub fn run(rules_dir: &Path) -> Result<(), CliError> {
43    let transforms = io_util::load_rules(rules_dir)?;
44    let discipline_n = super::discipline_gate::discipline_len()?;
45
46    println!("=== Validating {} ===", rules_dir.display());
47    println!("Transforms: {}", transforms.len());
48    println!(
49        "Discipline: {} rules (DC-01 .. DC-{:02})",
50        discipline_n, discipline_n
51    );
52    println!();
53
54    let report = super::discipline_gate::check(&transforms)?;
55
56    println!("Nodes:       {} (含 branch 内嵌子指令)", report.scanned);
57    println!();
58
59    if report.violations.is_empty() {
60        println!("[OK]      rule-set accepted by the discipline gate");
61    } else {
62        for v in &report.violations {
63            let label = if v.node_type.is_empty() {
64                "<missing type>"
65            } else {
66                v.node_type.as_str()
67            };
68            println!(
69                "[ERROR]   {:<24} {:<12} <- {} {}",
70                v.path, label, v.code, v.reason
71            );
72        }
73    }
74
75    println!();
76    println!("=== Summary ===");
77    println!("Violations: {}", report.violations.len());
78
79    if report.violations.is_empty() {
80        Ok(())
81    } else {
82        Err(CliError::other(format!(
83            "validation failed with {} violations",
84            report.violations.len()
85        )))
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    #![allow(clippy::unwrap_used)]
92    use evorule_tcb::META_INSTRUCTION_TYPES;
93
94    #[test]
95    fn test_whitelist_excludes_g8_words() {
96        // 确保白名单不含 G8 禁止词(conditional/while_loop/sequence)
97        // 否则 build.rs 会拦截本文件
98        for t in META_INSTRUCTION_TYPES {
99            assert!(
100                !matches!(*t, "conditional" | "while_loop" | "sequence"),
101                "whitelist must not contain G8-forbidden words: {}",
102                t
103            );
104        }
105    }
106
107    #[test]
108    fn test_whitelist_includes_core_meta_instructions() {
109        // 确保白名单包含 core_eval 核心元指令
110        assert!(META_INSTRUCTION_TYPES.contains(&"branch"));
111        assert!(META_INSTRUCTION_TYPES.contains(&"set"));
112        assert!(META_INSTRUCTION_TYPES.contains(&"push"));
113        assert!(META_INSTRUCTION_TYPES.contains(&"io_request"));
114        assert!(META_INSTRUCTION_TYPES.contains(&"enforce"));
115        // collect/merge 已退役,不得回流白名单
116        assert!(!META_INSTRUCTION_TYPES.contains(&"collect"));
117        assert!(!META_INSTRUCTION_TYPES.contains(&"merge"));
118        // P0-01:指令层类型不得混入元指令白名单
119        assert!(!META_INSTRUCTION_TYPES.contains(&"noop"));
120        assert!(!META_INSTRUCTION_TYPES.contains(&"increment"));
121        assert!(!META_INSTRUCTION_TYPES.contains(&"decrement"));
122    }
123}