Skip to main content

quanttide_work/
criterion.rs

1//! 判据:规则引擎那几种机械核对。
2//!
3//! 判据是**字段**,不是一行小语法:`path` 存在、`absent` 不存在、
4//! `file` + `contains` 含这段文字、`run` 这条命令退出码为零。
5//! 工具箱只把判据翻成「要跑什么」——真去跑(文件系统、起进程)是各自包的事。
6
7use crate::executor::{AGENT, HUMAN, RULE};
8use serde_yaml::{Mapping, Value as Yaml};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum RuleKind {
12    Path,
13    Absent,
14    Contains,
15    Run,
16}
17
18/// 一条判据:谁判、怎么判、说明。
19///
20/// 值对象,不可变。`rule` 四种判法各一个变体;`agent` / `human` 只有说明。
21/// 线上形状不变——进出定义文件时仍写成 `path` / `absent` / `file`+`contains` / `run` 字段。
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Criterion {
24    PathExists {
25        path: String,
26        description: String,
27    },
28    PathAbsent {
29        absent: String,
30        description: String,
31    },
32    FileContains {
33        file: String,
34        contains: String,
35        description: String,
36    },
37    CommandRun {
38        run: String,
39        description: String,
40    },
41    AgentJudgement {
42        description: String,
43    },
44    HumanGate {
45        description: String,
46    },
47}
48
49impl Criterion {
50    /// 谁判:`rule` / `agent` / `human`。
51    pub fn executor(&self) -> &'static str {
52        match self {
53            Criterion::PathExists { .. }
54            | Criterion::PathAbsent { .. }
55            | Criterion::FileContains { .. }
56            | Criterion::CommandRun { .. } => RULE,
57            Criterion::AgentJudgement { .. } => AGENT,
58            Criterion::HumanGate { .. } => HUMAN,
59        }
60    }
61
62    /// 写了的说明;空即按判法拼一句。
63    pub fn description(&self) -> &str {
64        match self {
65            Criterion::PathExists { description, .. }
66            | Criterion::PathAbsent { description, .. }
67            | Criterion::FileContains { description, .. }
68            | Criterion::CommandRun { description, .. }
69            | Criterion::AgentJudgement { description }
70            | Criterion::HumanGate { description } => description,
71        }
72    }
73
74    /// 人读的说明:写了就用写的,没写按判法拼。
75    pub fn text(&self) -> String {
76        if !self.description().is_empty() {
77            return self.description().to_string();
78        }
79        match self {
80            Criterion::PathExists { path, .. } => format!("存在:{path}"),
81            Criterion::PathAbsent { absent, .. } => format!("不存在:{absent}"),
82            Criterion::FileContains { file, contains, .. } => {
83                format!("含「{contains}」:{file}")
84            }
85            Criterion::CommandRun { run, .. } => format!("跑通:{run}"),
86            Criterion::AgentJudgement { .. } | Criterion::HumanGate { .. } => String::new(),
87        }
88    }
89
90    /// 写回定义里的字段形状。
91    pub fn to_yaml(&self) -> Yaml {
92        let mut pairs: Vec<(&str, &str)> = Vec::new();
93        match self {
94            Criterion::PathExists { path, description } => {
95                pairs.push(("executor", RULE));
96                pairs.push(("path", path));
97                if !description.is_empty() {
98                    pairs.push(("description", description));
99                }
100            }
101            Criterion::PathAbsent {
102                absent,
103                description,
104            } => {
105                pairs.push(("executor", RULE));
106                pairs.push(("absent", absent));
107                if !description.is_empty() {
108                    pairs.push(("description", description));
109                }
110            }
111            Criterion::FileContains {
112                file,
113                contains,
114                description,
115            } => {
116                pairs.push(("executor", RULE));
117                pairs.push(("file", file));
118                pairs.push(("contains", contains));
119                if !description.is_empty() {
120                    pairs.push(("description", description));
121                }
122            }
123            Criterion::CommandRun { run, description } => {
124                pairs.push(("executor", RULE));
125                pairs.push(("run", run));
126                if !description.is_empty() {
127                    pairs.push(("description", description));
128                }
129            }
130            Criterion::AgentJudgement { description } => {
131                pairs.push(("executor", AGENT));
132                pairs.push(("description", description));
133            }
134            Criterion::HumanGate { description } => {
135                pairs.push(("executor", HUMAN));
136                pairs.push(("description", description));
137            }
138        }
139        let mut map = Mapping::new();
140        for (key, value) in pairs {
141            map.insert(
142                Yaml::String(key.to_string()),
143                Yaml::String(value.to_string()),
144            );
145        }
146        Yaml::Mapping(map)
147    }
148
149    /// 占位展开:每个字段里的 `{{…}}` 交给 `expand` 换掉。
150    pub fn expanded<F>(&self, expand: F) -> Criterion
151    where
152        F: Fn(&str) -> String,
153    {
154        let ex = |value: &str| {
155            if value.contains("{{") {
156                expand(value)
157            } else {
158                value.to_string()
159            }
160        };
161        match self {
162            Criterion::PathExists { path, description } => Criterion::PathExists {
163                path: ex(path),
164                description: ex(description),
165            },
166            Criterion::PathAbsent {
167                absent,
168                description,
169            } => Criterion::PathAbsent {
170                absent: ex(absent),
171                description: ex(description),
172            },
173            Criterion::FileContains {
174                file,
175                contains,
176                description,
177            } => Criterion::FileContains {
178                file: ex(file),
179                contains: ex(contains),
180                description: ex(description),
181            },
182            Criterion::CommandRun { run, description } => Criterion::CommandRun {
183                run: ex(run),
184                description: ex(description),
185            },
186            Criterion::AgentJudgement { description } => Criterion::AgentJudgement {
187                description: ex(description),
188            },
189            Criterion::HumanGate { description } => Criterion::HumanGate {
190                description: ex(description),
191            },
192        }
193    }
194}
195
196/// 一条要跑的判据:说明 + 怎么判(`kind` 为空即不跑,交给智能体或人)。
197#[derive(Debug, Clone)]
198pub struct RuleItem {
199    pub description: String,
200    pub kind: Option<RuleKind>,
201    pub args: Vec<String>,
202}
203
204impl RuleItem {
205    pub fn machine(&self) -> bool {
206        self.kind.is_some()
207    }
208}
209
210/// 把判据翻成要跑的东西:rule 的跑,agent / human 的不跑。
211pub fn items_of(criteria: &[Criterion]) -> Vec<RuleItem> {
212    criteria
213        .iter()
214        .map(|criterion| {
215            let description = criterion.text();
216            let (kind, args) = match criterion {
217                Criterion::PathExists { path, .. } => (Some(RuleKind::Path), vec![path.clone()]),
218                Criterion::PathAbsent { absent, .. } => {
219                    (Some(RuleKind::Absent), vec![absent.clone()])
220                }
221                Criterion::FileContains { file, contains, .. } => (
222                    Some(RuleKind::Contains),
223                    vec![file.clone(), contains.clone()],
224                ),
225                Criterion::CommandRun { run, .. } => (Some(RuleKind::Run), vec![run.clone()]),
226                Criterion::AgentJudgement { .. } | Criterion::HumanGate { .. } => {
227                    (None, Vec::new())
228                }
229            };
230            RuleItem {
231                description,
232                kind,
233                args,
234            }
235        })
236        .collect()
237}