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/// `expected_version` = 调用方(插件 crate)显式传 `env!("CARGO_PKG_VERSION")` ——
62/// **Q1 不能在 audit 内自取 env!**:audit 的宏展开是 ek-core 自己的版本,
63/// 而未覆写 version() 的插件缺省值恰好也是 ek-core 版本,会把假匹配放行。
64///
65/// 判定面:Q1 version 显式覆写;Q2 routes 非空/去重(**服务面件**才必含请求线);
66/// Q3 services 目录唯一且全名合法;Q4 目录面有描述;Q5 配置面 namespace 合法。
67///
68/// **服务面形态挂钩**(0.4.3):`services()` 非空 = 面向服务的个体 → routes 必须
69/// 自报 `eReq.<名>`(否则服务请求永远进不来);`services()` 为空 = **纯订阅观察件**
70/// (ui 一类)→ 请求线自报与活体探针全部豁免(Q2/D4;写上只是死线)。
71///
72/// **配置面是可选件**(§9.1):`as_configurable()` 返回 None = 「无配置面」,
73/// 这是合法形态 —— Q5 仅在有面时生效,无面插件整类跳过,不产生任何 finding。
74/// 同理 §14.2 的配置面测试组也是**有面才要求**,无面插件整体豁免。
75#[must_use]
76pub fn shape_audit(plugin: &dyn Plugin, expected_version: &str) -> ShapeReport {
77    let mut rep = ShapeReport::default();
78    let name = plugin.name().to_string();
79
80    // Q1 version 覆写:七律 6(缺省实现会错填 ek-core 版本)。
81    if plugin.version().as_ref() != expected_version {
82        rep.fail(
83            "Q1",
84            format!(
85                "version() 必须显式覆写为本 crate 版本(七律 6);期望 {expected_version},实得 {}",
86                plugin.version()
87            ),
88        );
89    }
90
91    // Q2 routes:非空/去重;**服务面件**必含 eReq.<name>。
92    //
93    // 0.4.3(语义修正):`services()` 为空的**纯订阅观察件**(ui 一类,零服务面,
94    // 无人能 ask)合法跳过请求线自报 —— 写上是一条永远空驶的死线。判定挂钩:
95    // **有服务面才要求自报请求线**;零服务面 = 纯监听件,Q2 只判非空/去重。
96    let svcs = plugin.services();
97    let routes: Vec<String> = plugin.routes().iter().map(|c| c.to_string()).collect();
98    if routes.is_empty() {
99        rep.fail("Q2", "routes() 为空 = 无订阅面(至少自报请求线)");
100    } else {
101        let mut seen = std::collections::BTreeSet::new();
102        for r in &routes {
103            if !seen.insert(r.clone()) {
104                rep.fail("Q2", format!("routes() 重复订阅 {r}"));
105            }
106        }
107        if !svcs.is_empty() {
108            let req_line = e_req_type(&name);
109            if !routes.iter().any(|r| r == &req_line) {
110                rep.fail(
111                    "Q2",
112                    format!("routes() 必须自报请求线 {req_line};漏报 = 服务请求永远进不来"),
113                );
114            }
115        }
116    }
117
118    // Q3 services:目录唯一 + 全名 = `<实例名>.<后缀>`(§3 命名纪律)。
119    let mut ids = std::collections::BTreeSet::new();
120    for s in &svcs {
121        let id: &String = &s.id;
122        if !ids.insert(id.clone()) {
123            rep.fail("Q3", format!("服务目录重复 {id}"));
124        }
125        let expected = format!("{name}.");
126        if !id.starts_with(&expected) {
127            rep.fail("Q3", format!("服务全名未挂实例名前缀: {id}"));
128        }
129    }
130
131    // Q4 描述非空(消费方目录可读性)。
132    for s in &svcs {
133        if s.doc.trim().is_empty() {
134            rep.fail("Q4", format!("服务 {} 描述为空", s.id));
135        }
136    }
137
138    // Q5 配置面 namespace 合法(§9.2)。**None = 无配置面 = 合法(可选件),不判。**
139    if let Some(cfg) = plugin.as_configurable() {
140        if !valid_config_ns(cfg.config_ns()) {
141            rep.fail("Q5", format!("config_ns 非法: {}", cfg.config_ns()));
142        }
143    }
144
145    rep
146}
147
148/// 装配自检探针载荷(故意未注册类型;预期全体 miss → InvalidPayload)。
149struct AuditProbeReq;
150
151/// 装配自检报告(装配期逐项;列同 ShapeReport 的 findings)。
152#[derive(Debug, Default)]
153pub struct DeployReport {
154    /// 全部判定。
155    pub findings: Vec<Finding>,
156}
157
158impl DeployReport {
159    fn fail(&mut self, id: &'static str, text: impl Into<String>) {
160        self.findings.push(Finding {
161            id,
162            severity: Severity::Fail,
163            text: text.into(),
164        });
165    }
166
167    /// 全绿判定。
168    #[must_use]
169    pub fn is_ok(&self) -> bool {
170        self.findings.is_empty()
171    }
172}
173
174/// 装配自检:最小宿主真实装配 → 起停 → 服务目录可见 → 请求线活体探测。
175///
176/// 探测语义:向 `eReq.<名>` 投一个**故意未注册**的载荷,插件方 arm 全 miss
177/// 必回 `Err(ReplyErr::InvalidPayload)`(契约错型显形,`ENV` 侧语义对齐 §7)。
178/// 返回 [`DeployReport`];探针超时即判坏(请求线未自报/未装配/宿主路由断)。
179///
180/// 0.4.3(语义修正,与 shape Q2 挂钩):`services()` 为空的纯订阅观察件
181/// **免探针**(无服务面 = 无可被 ask 的口,探针超时对它不是缺陷)——只判
182/// D1 装配 / D2 起动 / D3 在册。
183pub fn deploy_audit<T: Plugin + 'static>(plugin: Arc<T>) -> DeployReport {
184    deploy_audit_with_peers(plugin, Vec::new())
185}
186
187/// 装配自检同伴壳(0.5.4):把 `Arc<dyn Plugin>` 抬成 `add_plugin` 泛型点
188/// 可用的具体类型(全方法转发,不遮身份面)。
189struct PeerWrap(Arc<dyn Plugin>);
190
191impl Plugin for PeerWrap {
192    fn name(&self) -> &str {
193        self.0.name()
194    }
195    fn deps(&self) -> &[&str] {
196        self.0.deps()
197    }
198    fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
199        self.0.routes()
200    }
201    fn mailbox_soft_cap(&self) -> u64 {
202        self.0.mailbox_soft_cap()
203    }
204    fn version(&self) -> std::borrow::Cow<'static, str> {
205        self.0.version()
206    }
207    fn as_any(&self) -> Option<&dyn std::any::Any> {
208        self.0.as_any()
209    }
210    fn as_configurable(&self) -> Option<&dyn crate::section::Configurable> {
211        self.0.as_configurable()
212    }
213    fn services(&self) -> Vec<crate::service::ServiceDesc> {
214        self.0.services()
215    }
216    fn on_init(&self, ctx: &crate::context::PluginContext) {
217        self.0.on_init(ctx);
218    }
219    fn on_start(&self) {
220        self.0.on_start();
221    }
222    fn on_stop(&self) {
223        self.0.on_stop();
224    }
225    fn on_mail(&self, event: &ek_event::Event) {
226        self.0.on_mail(event);
227    }
228}
229
230/// 装配自检(**带同伴**,0.5.4 / 升级方案步骤 2 口径):目标插件的 deps
231/// 指向同伴(如全插件必依赖的 `"cfg"`)时,`deploy_audit` 自建宿主必报
232/// `UnknownDep` = 闸口假红。本口**按给出顺序先装配同伴**(例:cfg 收件壳),
233/// 再装配目标,然后对整宿主跑同一套 D1-D4 审计(判据语义不减不放宽);
234/// `deploy_audit` = 空同伴的特例(无依赖插件零移植)。
235pub fn deploy_audit_with_peers<T: Plugin + 'static>(
236    plugin: Arc<T>,
237    peers: Vec<Arc<dyn Plugin>>,
238) -> DeployReport {
239    let mut rep = DeployReport::default();
240    let name = plugin.name().to_string();
241    let has_service_face = !plugin.services().is_empty();
242    let ev = Arc::new(EventEngine::new());
243    let host = MainEngine::new(Some(ev.clone()));
244    for p in peers {
245        if let Err(e) = host.add_plugin(Arc::new(PeerWrap(p))) {
246            rep.fail("D1", format!("同伴装配失败: {e}"));
247            return rep;
248        }
249    }
250    if let Err(e) = host.add_plugin(plugin) {
251        rep.fail("D1", format!("add_plugin 失败: {e}"));
252        return rep;
253    }
254    if let Err(e) = host.start() {
255        rep.fail("D2", format!("host.start 失败: {e}"));
256        return rep;
257    }
258
259    // D3 实例在册(装配期登记簿点名; PluginInfo 无目录字段,目录可见性走 D4)。
260    if host.get_state(&name).is_none() {
261        rep.fail("D3", "实例未在册(装配失败)");
262    }
263
264    // D4 请求线活体探测(**仅有服务面时**)。
265    if has_service_face {
266        let (env, rx) = Envelope::new(Arc::new(AuditProbeReq) as Arc<dyn Any + Send + Sync>);
267        let put = ev.put(Event::with_data(e_req_type(&name), Arc::new(env)));
268        if let Err(e) = put {
269            rep.fail("D4", format!("总线投递失败: {e:?}"));
270        } else {
271            match rx.recv_timeout(Duration::from_secs(10)) {
272                Ok(boxed) => {
273                    let typed = boxed.downcast::<Result<(), ReplyErr>>();
274                    match typed {
275                        Ok(inner) => {
276                            if matches!(*inner, Err(ReplyErr::InvalidPayload)) {
277                                // 期望形态:InvalidPayload = arm 全 miss 显形 = 请求线活。
278                            } else {
279                                rep.fail(
280                                    "D4",
281                                    "探针未注册载荷竟然回了非 InvalidPayload 形态(契约应答面失真)",
282                                );
283                            }
284                        }
285                        Err(_) => {
286                            // 剥包错型=同样 InvalidPayload 语义(允许实现走 Box 擦除路径)。
287                        }
288                    }
289                }
290                Err(_) => rep.fail("D4", "请求探针超时:请求线未自报(routes 漏报)或分派断"),
291            }
292        }
293        // has_service_face 收口(零服务面纯订阅件:D1-D3 即封顶)
294    }
295    // 收尾:静默撤下(五步;不判成败,只保证不悬挂)。
296    host.close(Duration::from_secs(2));
297    rep
298}