Skip to main content

ek_core/
audit.rs

1//! 通用插件自检闸(结构自检 + 装配自检;零业务;GUIDE §16 双跑纪律)。
2//!
3//! 用法(两层闸,任何插件通用):
4//! 1. **骨架期(业务零)先跑一次**:结构自检 + 装配自检全绿 = 框架件达标;
5//! 2. **业务加入后再跑一次**:同样两绿 = 结构未被业务破坏。
6//!
7//! 本模块**只读**,不触碰业务状态;判据与 GUIDE 纪律一一对应(锚点见各条注释)。
8
9use crate::envelope::{Envelope, ReplyErr, e_req_type};
10use crate::main_engine::MainEngine;
11use crate::plugin::Plugin;
12use crate::section::valid_config_ns;
13use ek_event::{Event, EventEngine};
14use std::any::Any;
15use std::sync::Arc;
16use std::time::Duration;
17
18/// 判定等级(只报 Fail;Warn 预留)。
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum Severity {
21    /// 不合格。
22    Fail,
23}
24
25/// 单条判定(id 稳定,可对账;文本点名原因)。
26#[derive(Debug, Clone)]
27pub struct Finding {
28    /// 稳定 id(如 `Q1`);跨版本只增不减。
29    pub id: &'static str,
30    /// 等级。
31    pub severity: Severity,
32    /// 判定说明。
33    pub text: String,
34}
35
36/// 自检报告(`is_ok` = 全绿)。
37#[derive(Debug, Default)]
38pub struct ShapeReport {
39    /// 全部判定(空 = 无发现)。
40    pub findings: Vec<Finding>,
41}
42
43impl ShapeReport {
44    fn fail(&mut self, id: &'static str, text: impl Into<String>) {
45        self.findings.push(Finding {
46            id,
47            severity: Severity::Fail,
48            text: text.into(),
49        });
50    }
51
52    /// 全绿判定。
53    #[must_use]
54    pub fn is_ok(&self) -> bool {
55        self.findings.is_empty()
56    }
57}
58
59/// 结构自检(不装配、不起宿主;对插件契约逐项判定;§5 F4/§7)。
60///
61/// 判定面:Q1 version 显式覆写;Q2 routes 非空/去重/自报请求线;
62/// Q3 services 目录唯一且全名合法;Q4 目录面有描述;Q5 配置面 namespace 合法。
63#[must_use]
64pub fn shape_audit(plugin: &dyn Plugin) -> ShapeReport {
65    let mut rep = ShapeReport::default();
66    let name = plugin.name().to_string();
67
68    // Q1 version 覆写:七律 6(缺省实现会错填 ek-core 版本)。
69    if plugin.version() != std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION")) {
70        rep.fail("Q1", "version() 必须显式覆写为本 crate 版本(七律 6)");
71    }
72
73    // Q2 routes:非空/去重/必含 eReq.<name>(§7 入口面)。
74    let routes: Vec<String> = plugin.routes().iter().map(|c| c.to_string()).collect();
75    if routes.is_empty() {
76        rep.fail("Q2", "routes() 为空 = 无订阅面(至少自报请求线)");
77    } else {
78        let mut seen = std::collections::BTreeSet::new();
79        for r in &routes {
80            if !seen.insert(r.clone()) {
81                rep.fail("Q2", format!("routes() 重复订阅 {r}"));
82            }
83        }
84        let req_line = e_req_type(&name);
85        if !routes.iter().any(|r| r == &req_line) {
86            rep.fail(
87                "Q2",
88                format!("routes() 必须自报请求线 {req_line};漏报 = 服务请求永远进不来"),
89            );
90        }
91    }
92
93    // Q3 services:目录唯一 + 全名 = `<实例名>.<后缀>`(§3 命名纪律)。
94    let svcs = plugin.services();
95    let mut ids = std::collections::BTreeSet::new();
96    for s in &svcs {
97        let id: &String = &s.id;
98        if !ids.insert(id.clone()) {
99            rep.fail("Q3", format!("服务目录重复 {id}"));
100        }
101        let expected = format!("{name}.");
102        if !id.starts_with(&expected) {
103            rep.fail("Q3", format!("服务全名未挂实例名前缀: {id}"));
104        }
105    }
106
107    // Q4 描述非空(消费方目录可读性)。
108    for s in &svcs {
109        if s.doc.trim().is_empty() {
110            rep.fail("Q4", format!("服务 {} 描述为空", s.id));
111        }
112    }
113
114    // Q5 配置面 namespace 合法(§9.2)。
115    if let Some(cfg) = plugin.as_configurable() {
116        if !valid_config_ns(cfg.config_ns()) {
117            rep.fail("Q5", format!("config_ns 非法: {}", cfg.config_ns()));
118        }
119    }
120
121    rep
122}
123
124/// 装配自检探针载荷(故意未注册类型;预期全体 miss → InvalidPayload)。
125struct AuditProbeReq;
126
127/// 装配自检报告(装配期逐项;列同 ShapeReport 的 findings)。
128#[derive(Debug, Default)]
129pub struct DeployReport {
130    /// 全部判定。
131    pub findings: Vec<Finding>,
132}
133
134impl DeployReport {
135    fn fail(&mut self, id: &'static str, text: impl Into<String>) {
136        self.findings.push(Finding {
137            id,
138            severity: Severity::Fail,
139            text: text.into(),
140        });
141    }
142
143    /// 全绿判定。
144    #[must_use]
145    pub fn is_ok(&self) -> bool {
146        self.findings.is_empty()
147    }
148}
149
150/// 装配自检:最小宿主真实装配 → 起停 → 服务目录可见 → 请求线活体探测。
151///
152/// 探测语义:向 `eReq.<名>` 投一个**故意未注册**的载荷,插件方 arm 全 miss
153/// 必回 `Err(ReplyErr::InvalidPayload)`(契约错型显形,`ENV` 侧语义对齐 §7)。
154/// 返回 [`DeployReport`];探针超时即判坏(请求线未自报/未装配/宿主路由断)。
155pub fn deploy_audit<T: Plugin + 'static>(plugin: Arc<T>) -> DeployReport {
156    let mut rep = DeployReport::default();
157    let name = plugin.name().to_string();
158    let ev = Arc::new(EventEngine::new());
159    let host = MainEngine::new(Some(ev.clone()));
160    if let Err(e) = host.add_plugin(plugin) {
161        rep.fail("D1", format!("add_plugin 失败: {e}"));
162        return rep;
163    }
164    if let Err(e) = host.start() {
165        rep.fail("D2", format!("host.start 失败: {e}"));
166        return rep;
167    }
168
169    // D3 实例在册(装配期登记簿点名;PluginInfo 无目录字段,目录可见性走 D4)。
170    if host.get_state(&name).is_none() {
171        rep.fail("D3", "实例未在册(装配失败)");
172    }
173
174    // D4 请求线活体探测。
175    let (env, rx) = Envelope::new(Arc::new(AuditProbeReq) as Arc<dyn Any + Send + Sync>);
176    let put = ev.put(Event::with_data(e_req_type(&name), Arc::new(env)));
177    if let Err(e) = put {
178        rep.fail("D4", format!("总线投递失败: {e:?}"));
179    } else {
180        match rx.recv_timeout(Duration::from_secs(10)) {
181            Ok(boxed) => {
182                let typed = boxed.downcast::<Result<(), ReplyErr>>();
183                match typed {
184                    Ok(inner) => {
185                        if matches!(*inner, Err(ReplyErr::InvalidPayload)) {
186                            // 期望形态:InvalidPayload = arm 全 miss 显形 = 请求线活。
187                        } else {
188                            rep.fail(
189                                "D4",
190                                "探针未注册载荷竟然回了非 InvalidPayload 形态(契约应答面失真)",
191                            );
192                        }
193                    }
194                    Err(_) => {
195                        // 剥包错型=同样 InvalidPayload 语义(允许实现走 Box 擦除路径)。
196                    }
197                }
198            }
199            Err(_) => rep.fail("D4", "请求探针超时:请求线未自报(routes 漏报)或分派断"),
200        }
201    }
202
203    // 收尾:静默撤下(五步;不判成败,只保证不悬挂)。
204    host.close(Duration::from_secs(2));
205    rep
206}