evorule-discipline 0.12.0

EvoRule L2 规则集形态纪律门禁 —— 机制侧遍历/事实标注 + 内核求值(纪律数据 SSOT 在 evorule-tcb)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 EvoRule Project
// This file is part of EvoRule, licensed under GNU Affero General Public License v3 or later.
//! 规则集形态门禁(L2 纪律)—— 判定引擎(机制侧)
//!
//! # 机制 / 策略分离
//!
//! - **策略(可变)**:`evorule-tcb/discipline/core_eval.json` —— 9 条 DC 纪律,
//!   每条是带 `reason` 的 `enforce` 规则。改纪律 = 改 JSON,不动 Rust。
//! - **机制(不可变)**:本 crate —— 遍历规则集、把内核看不见的「列表级事实」
//!   标注进 `instruction._ctx`,然后交给 `execute_transition` 求值。
//!
//! `core_eval` 没有循环原语(元指令全集仅 branch/set/push/io_request/enforce),
//! 因此遍历必须由外层驱动;**判定全部在 JSON**,这是纪律可版本化、可评审的前提。
//!
//! # 为什么是前置而非运行时
//!
//! 运行时做会污染 `rule_hits` 归因口径、挤占 64 规则 / 1024 指令预算;且规则集
//! 装载后不再变化,每次转换都校验纯属浪费。更关键的是语义隔离:装载期校验失败
//! 的表现是「拒载,规则集没加载起来」,运行时校验失败的表现是业务 `Violation`
//! 事实入链——**两者在审计链上长得一模一样**,规则写得丑会和真的违宪混在一起。
//!
//! # 使用方(B 段接线,design-06 §11)
//!
//! - `evorule validate`(evorule-cli,薄壳命令)——开发者/CI 前置;
//! - `evorule-server` 装载期门禁(`parse_rule_file` + `load_core_eval_transforms`
//!   两个接线点)——拦截 LLM 补丁热重载 / bundle 导入 / 宪法装载的威胁路径。
//!
//! # 边界
//!
//! 这是**形态**门禁,不是语义证明。它拦得住「结构会让守卫失效」,拦不住
//! 「规则写错了业务约束」;也不证明 `execute_transition` 自身正确(那是 TCB_SPEC
//! 的 T/G/D 系列与 Kani 的职责)。用 TCB 检查喂给 TCB 的规则属于自证
//! (见 evorule-tcb/src/discipline.rs「诚实边界」),与落点无关。

use std::collections::BTreeSet;

use evorule_tcb::discipline::DISCIPLINE_CORE_EVAL;
use evorule_tcb::{execute_transition, JsonValue, TransitionResult};

/// 门禁求值失败(纪律数据损坏 / 内核求值异常)。非违规——违规是 `Report`。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisciplineError(pub String);

impl std::fmt::Display for DisciplineError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "discipline gate error: {}", self.0)
    }
}

impl std::error::Error for DisciplineError {}

/// 一条纪律违规
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
    /// 节点在被校验规则集中的位置,如 `[0]` / `[0].on_true[1]`
    pub path: String,
    /// 节点的 `type` 字段(缺失时为空串)
    pub node_type: String,
    /// 纪律条款号,如 `DC-05`
    pub code: String,
    /// 违规说明(取自纪律条款的 `params.reason`,可直接进审计报告)
    pub reason: String,
}

/// 加载嵌入的纪律规则集
///
/// 复用 `evorule-reactor::serde_to_tcb` 做序列化,与 WAL / auditor 走同一条
/// 转换路径,避免「门禁看到的 JSON 与内核看到的 JSON 不一致」。
fn load_discipline() -> Result<Vec<JsonValue>, DisciplineError> {
    let raw: serde_json::Value = serde_json::from_str(DISCIPLINE_CORE_EVAL).map_err(|e| {
        DisciplineError(format!(
            "embedded discipline core_eval is not valid JSON: {e}"
        ))
    })?;
    let arr = raw.as_array().ok_or_else(|| {
        DisciplineError("embedded discipline core_eval must be a JSON array of rules".to_string())
    })?;
    Ok(arr.iter().map(evorule_reactor::serde_to_tcb).collect())
}

/// 纪律条款数量(供报告头展示)
pub fn discipline_len() -> Result<usize, DisciplineError> {
    Ok(load_discipline()?.len())
}

/// 门禁报告
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Report {
    /// 被检查的节点总数(含 branch 内的嵌套子指令)
    pub scanned: usize,
    /// 违规列表(空 = 规则集合规,通过门禁)
    pub violations: Vec<Violation>,
}

/// 对被校验规则集执行全部纪律条款
pub fn check(ruleset: &[JsonValue]) -> Result<Report, DisciplineError> {
    let discipline = load_discipline()?;
    let tops: Vec<TopFacts> = ruleset.iter().map(build_top_facts).collect();
    let nodes = flatten(ruleset, &tops);

    let mut violations = Vec::new();
    for nd in &nodes {
        let probe = build_probe(nd, &tops);
        // 纪律条款在数组中的下标 + 1 == DC 编号(见 core_eval.json 的顺序约定)
        match execute_transition(&discipline, &probe, &JsonValue::Null, &[]) {
            Ok(TransitionResult::Halted { rule_index, reason }) => violations.push(Violation {
                path: nd.path.clone(),
                node_type: node_type(nd.node).to_string(),
                code: {
                    let n = rule_index.saturating_add(1);
                    format!("DC-{n:02}")
                },
                reason,
            }),
            Ok(_) => {}
            Err(e) => {
                return Err(DisciplineError(format!(
                    "discipline evaluation failed at {}: {e:?}",
                    nd.path
                )))
            }
        }
    }
    Ok(Report {
        scanned: nodes.len(),
        violations,
    })
}

// ---------------------------------------------------------------- 遍历与结构事实

fn node_type(n: &JsonValue) -> &str {
    n.get("type").and_then(JsonValue::as_str).unwrap_or("")
}

/// `params` 是否含有某个 key —— **字段存在性**,与值是否为 null 无关
///
/// 为什么需要它:domain 的 `exists` 谓词语义是「null 视为已清除」,
/// 无法表达「有这个字段,只是值是 null」。而 `set value: null`(清空 I/O
/// 结果)正是受支持的合法写法(见 domain.rs 模块文档),故 DC-06 必须按
/// 字段存在性而非值非空判定。
fn params_has_key(n: &JsonValue, key: &str) -> bool {
    n.get("params")
        .and_then(JsonValue::as_object)
        .map(|o| o.contains_key(key))
        .unwrap_or(false)
}

fn children_of<'a>(n: &'a JsonValue, key: &str) -> Vec<&'a JsonValue> {
    match n
        .get("params")
        .and_then(|p| p.get(key))
        .and_then(JsonValue::as_array)
    {
        Some(a) => a.iter().collect(),
        None => Vec::new(),
    }
}

/// 从 domain 递归收集 `instruction_type` 目标集合;空集 = 不设限(ANY)
fn collect_targets(domain: &JsonValue, out: &mut BTreeSet<String>) {
    match domain.get("type").and_then(JsonValue::as_str) {
        Some("instruction") => {
            if let Some(s) = domain.get("instruction_type").and_then(JsonValue::as_str) {
                out.insert(s.to_string());
            }
        }
        Some("all") => {
            if let Some(JsonValue::Array(items)) = domain.get("inner") {
                for d in items {
                    collect_targets(d, out);
                }
            }
        }
        Some("not") => {
            if let Some(inner) = domain.get("inner") {
                collect_targets(inner, out);
            }
        }
        _ => {}
    }
}

fn top_targets(rule: &JsonValue) -> BTreeSet<String> {
    let mut s = BTreeSet::new();
    if let Some(d) = rule.get("params").and_then(|p| p.get("domain")) {
        collect_targets(d, &mut s);
    }
    s
}

/// 从 domain 收集**可证明的合取式** `instruction_type` 目标集合
///
/// 与 [`collect_targets`] 的区别:**不下钻 `not`**。`not(instruction X)` 的语义是
/// 「类型不是 X」,把 X 收进目标集合对遮蔽判定(交集=可能同命中)是保守的,
/// 但对「同趟可达」判定(交集=可能同时执行)方向相反——下钻 `not` 会把
/// 反目标当正目标,推出不健全的「不互斥/不可达」。此处宁可空集(= ANY)。
fn provable_targets(rule: &JsonValue) -> BTreeSet<String> {
    fn walk(domain: &JsonValue, out: &mut BTreeSet<String>) {
        match domain.get("type").and_then(JsonValue::as_str) {
            Some("instruction") => {
                if let Some(s) = domain.get("instruction_type").and_then(JsonValue::as_str) {
                    out.insert(s.to_string());
                }
            }
            // 仅合取语境(顶层单谓词 / all)可证明「命中 ⇒ 类型 ∈ 集合」
            Some("all") => {
                if let Some(JsonValue::Array(items)) = domain.get("inner") {
                    for d in items {
                        walk(d, out);
                    }
                }
            }
            _ => {}
        }
    }
    let mut s = BTreeSet::<String>::new();
    if let Some(d) = rule.get("params").and_then(|p| p.get("domain")) {
        walk(d, &mut s);
    }
    s
}

/// 早退规则:自身或其子指令会产生 IoRequired / Halted 信号(传播即停)
fn is_early_exit(n: &JsonValue) -> bool {
    match node_type(n) {
        "enforce" | "io_request" => true,
        "branch" => ["on_true", "on_false"]
            .iter()
            .any(|k| children_of(n, k).iter().any(|c| is_early_exit(c))),
        _ => false,
    }
}

/// 子树中 io_request 的个数(D4:一条规则最多一个 I/O)
fn count_io(n: &JsonValue) -> usize {
    let mut c = usize::from(node_type(n) == "io_request");
    if node_type(n) == "branch" {
        for key in ["on_true", "on_false"] {
            for ch in children_of(n, key) {
                c += count_io(ch);
            }
        }
    }
    c
}

/// 子树是否含改状态操作(set / push)—— D5 判据
fn has_mutating(n: &JsonValue) -> bool {
    if matches!(node_type(n), "set" | "push") {
        return true;
    }
    node_type(n) == "branch"
        && ["on_true", "on_false"]
            .iter()
            .any(|k| children_of(n, k).iter().any(|c| has_mutating(c)))
}

/// 目标集合是否可能相交(空集视为 ANY → 保守判 true)
fn maybe_overlap(a: &BTreeSet<String>, b: &BTreeSet<String>) -> bool {
    a.is_empty() || b.is_empty() || a.intersection(b).next().is_some()
}

/// 一条顶层规则的结构事实(供遮蔽判定与同趟可达判定共用)
struct TopFacts {
    /// 早退规则:自身或其子指令会产生信号(传播即停)
    shadow_source: bool,
    /// domain 的 instruction_type 目标集合(空集 = ANY;含 not 下降,DC-05 语义)
    targets: BTreeSet<String>,
    /// 可证明的合取式指令类型目标集(不下钻 not;同趟判定专用)
    provable: BTreeSet<String>,
    /// domain 中**正**存在断言的路径(`exists P`)
    pos_exists: BTreeSet<String>,
    /// domain 中**负**存在断言的路径(`not(exists P)`)
    neg_exists: BTreeSet<String>,
}

/// 构造单条顶层规则的结构事实
fn build_top_facts(r: &JsonValue) -> TopFacts {
    let mut pos = BTreeSet::new();
    let mut neg = BTreeSet::new();
    if let Some(d) = r.get("params").and_then(|p| p.get("domain")) {
        collect_exists_paths(d, false, &mut pos, &mut neg);
    }
    TopFacts {
        shadow_source: is_shadow_source(r),
        targets: top_targets(r),
        provable: provable_targets(r),
        pos_exists: pos,
        neg_exists: neg,
    }
}

/// 收集 domain 中的正/负 exists 路径
///
/// **只在可证明的语境下收集**(report-006 F1 / R-A4 精度修复):
/// - 顶层与 `all` 是合取:`exists P` ⇒ 正断言;
/// - `not(exists P)` ⇒ 负断言(仅下钻一层取反);
/// - 其余嵌套(`not(all)`、`not(not)` 等)**不下钻**——`not(A ∧ B)` 推不出
///   `¬A`,冒然收集会引入不健全的"互斥",宁可保持保守。
fn collect_exists_paths(
    domain: &JsonValue,
    negated: bool,
    pos: &mut BTreeSet<String>,
    neg: &mut BTreeSet<String>,
) {
    match domain.get("type").and_then(JsonValue::as_str) {
        Some("exists") => {
            if let Some(p) = domain.get("path").and_then(JsonValue::as_str) {
                if negated {
                    neg.insert(p.to_string());
                } else {
                    pos.insert(p.to_string());
                }
            }
        }
        Some("all") => {
            if let Some(JsonValue::Array(items)) = domain.get("inner") {
                for d in items {
                    collect_exists_paths(d, negated, pos, neg);
                }
            }
        }
        // 仅 `not(exists P)` 可证明为负断言;其余 not 内不下钻
        Some("not")
            if domain
                .get("inner")
                .and_then(|i| i.get("type"))
                .and_then(JsonValue::as_str)
                == Some("exists") =>
        {
            if let Some(inner) = domain.get("inner") {
                collect_exists_paths(inner, !negated, pos, neg);
            }
        }
        _ => {}
    }
}

/// 两规则 domain 是否**可证明互斥**
///
/// 判据:存在同一路径 P,一方断言 `exists P`、另一方断言 `not(exists P)`。
/// 此时两规则不可能同时命中 ⇒ 早退方构不成对对方的遮蔽。
/// 这是单向安全的精化:只**减少** shadowed=true 的判定,不新增。
fn mutually_exclusive(a: &TopFacts, b: &TopFacts) -> bool {
    !a.pos_exists.is_disjoint(&b.neg_exists) || !b.pos_exists.is_disjoint(&a.neg_exists)
}

/// 遮蔽源:会抢先返回信号、使后续规则永不求值的顶层规则
///
/// **必须与 TCB 内核语义同步**:BUG-P0-005 修复后顶层 `enforce` 走「约束前置门」
/// 独立求值(约束不是状态变换),不再参与序列内的早退竞争,因此**不是**遮蔽源。
fn is_shadow_source(top_rule: &JsonValue) -> bool {
    match node_type(top_rule) {
        "enforce" => false,
        _ => is_early_exit(top_rule),
    }
}

/// 被校验的一个节点(含所属顶层规则与列表位置等「内核看不见」的事实)
struct Node<'a> {
    top: usize,
    path: String,
    node: &'a JsonValue,
    nested: bool,
    targets: BTreeSet<String>,
    /// 所属顶层规则树内的 io_request 总数(D4)
    io_in_tree: usize,
    /// 同一趟转换中,它之前是否存在可达的改状态操作(D5)
    ///
    /// 顶层规则:仅统计**同趟可达**的前序规则(domain 指令类型可证不相交的
    /// 前序规则在本趟必不执行,见 `flatten` 内注释);嵌套子节点:继承父链
    /// 事实 + 本级同级前缀(DFS 序单调过近似,见 `child_node` 注释)。
    preceded_by_mutating: bool,
}

/// 前序遍历:父节点先入列,再递归 branch 的两个分支
///
/// 节点自身的 `preceded_by_mutating` 在构造时已定型(顶层规则 = 同趟可达精化,
/// 嵌套子节点 = 同级原始前缀),walk 只透传、不重算。
fn walk<'a>(nd: &Node<'a>, out: &mut Vec<Node<'a>>) {
    out.push(Node {
        top: nd.top,
        path: nd.path.clone(),
        node: nd.node,
        nested: nd.nested,
        targets: nd.targets.clone(),
        io_in_tree: nd.io_in_tree,
        preceded_by_mutating: nd.preceded_by_mutating,
    });
    if node_type(nd.node) == "branch" {
        for key in ["on_true", "on_false"] {
            let children = children_of(nd.node, key);
            for (j, c) in children.iter().enumerate() {
                let prior: Vec<&'a JsonValue> = children.iter().take(j).copied().collect();
                let child = child_node(nd, format!("{}.{key}[{j}]", nd.path), c, true, &prior);
                walk(&child, out);
            }
        }
    }
}

/// 从父节点派生子节点描述符(继承顶层下标与目标集合,重置同级前缀)
///
/// 同趟事实 = **继承父链** ∪ **本级同级前缀**:branch 是 if/else(两臂互斥,
/// 不存在「另一臂的 set 在我之后执行」的漏算),DFS 序中父链前的事实必然
/// 先于本节点——继承是单调的过近似(多报不漏报,保守方向正确)。
/// 跨顶层规则的嵌套同趟 set(R-B1 验证中发现的盲区)由此覆盖。
fn child_node<'a>(
    parent: &Node<'a>,
    path: String,
    node: &'a JsonValue,
    nested: bool,
    before: &[&'a JsonValue],
) -> Node<'a> {
    Node {
        top: parent.top,
        path,
        node,
        nested,
        targets: parent.targets.clone(),
        io_in_tree: parent.io_in_tree,
        preceded_by_mutating: parent.preceded_by_mutating || before.iter().any(|s| has_mutating(s)),
    }
}

fn flatten<'a>(rules: &'a [JsonValue], tops: &[TopFacts]) -> Vec<Node<'a>> {
    let mut out = Vec::new();
    for (i, r) in rules.iter().enumerate() {
        let before: Vec<&JsonValue> = rules.iter().take(i).collect();
        // 同趟可达精化(report-006 F2 / R-B1 两段式整改的前提):
        // 一个 transform 内的顶层规则共享同一转换趟,前序规则的 set/push 对
        // 后序规则的 io 而言是「io 前状态修改」(IoRequired 纯信号会一并丢弃)。
        // 但指令类型每趟唯一,前序规则在下列**可证明不可能与本规则同趟命中**时,
        // 其变更构不成同趟前置:
        //   a) domain 合取式指令类型目标集与本规则可证不相交(双方非空);
        //   b) 两规则 domain 存在同路径正/负 exists 互斥(R-A4 同一判据)。
        // 判定只在机制侧供事实,DC 条款 JSON 不动。
        let preceded_by_mutating = before.iter().enumerate().any(|(j, s)| {
            has_mutating(s)
                && maybe_overlap(&tops[j].provable, &tops[i].provable)
                && !mutually_exclusive(&tops[j], &tops[i])
        });
        let root = Node {
            top: i,
            path: format!("[{i}]"),
            node: r,
            nested: false,
            targets: tops[i].targets.clone(),
            io_in_tree: count_io(r),
            preceded_by_mutating,
        };
        walk(&root, &mut out);
    }
    out
}

/// 把「列表级事实」标注进节点副本,交给内核求值(原规则集不被修改)
fn build_probe(nd: &Node<'_>, tops: &[TopFacts]) -> JsonValue {
    let owner = tops.get(nd.top);
    let shadowed = node_type(nd.node) == "io_request"
        && (0..nd.top).any(|j| match tops.get(j) {
            Some(t) => {
                t.shadow_source
                    && maybe_overlap(&t.targets, &nd.targets)
                    // 互斥精化(R-A4 / report-006 F1):source 与 owner 的 domain
                    // 在同一路径上正/负 exists 互斥 ⇒ 不可能同时命中,无遮蔽
                    && !owner.is_some_and(|o| mutually_exclusive(t, o))
            }
            None => false,
        });

    let mut probe = nd.node.clone();
    let ctx = JsonValue::object_from_pairs(&[
        ("path", JsonValue::string(nd.path.clone())),
        ("nested", JsonValue::bool(nd.nested)),
        ("shadowed", JsonValue::bool(shadowed)),
        (
            "io_in_tree",
            JsonValue::integer(i64::try_from(nd.io_in_tree).unwrap_or(i64::MAX)),
        ),
        (
            "preceded_by_mutating",
            JsonValue::bool(nd.preceded_by_mutating),
        ),
        ("has_attr", JsonValue::bool(params_has_key(nd.node, "attr"))),
        (
            "has_value",
            JsonValue::bool(params_has_key(nd.node, "value")),
        ),
    ]);
    probe.insert("_ctx".to_string(), ctx);
    probe
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used)]
    use super::*;

    /// 用纪律自身的序列化路径构造被校验规则集(与 load_rules 同源)
    fn ruleset(json: &str) -> Vec<JsonValue> {
        let v: serde_json::Value = serde_json::from_str(json).unwrap();
        match v {
            serde_json::Value::Array(a) => a.iter().map(evorule_reactor::serde_to_tcb).collect(),
            other => vec![evorule_reactor::serde_to_tcb(&other)],
        }
    }

    fn codes(violations: &[Violation]) -> Vec<&str> {
        violations.iter().map(|v| v.code.as_str()).collect()
    }

    const GOOD: &str = r#"[
      {"type":"enforce","params":{"domain":{"type":"instruction","instruction_type":"set"},"reason":"r1"}},
      {"type":"branch","params":{"domain":{"type":"exists","path":"a"},
        "on_true":[{"type":"set","params":{"attr":"x","value":1}}],
        "on_false":[{"type":"io_request","params":{"io_type":"llm"}}]}}
    ]"#;

    #[test]
    fn good_ruleset_passes_the_gate() {
        let report = check(&ruleset(GOOD)).unwrap();
        let v = report.violations;
        assert!(report.scanned > 0, "应实际扫描到节点");
        assert!(v.is_empty(), "合规规则集不得被拒载: {v:?}");
    }

    #[test]
    fn unknown_meta_instruction_is_rejected() {
        let v = check(&ruleset(r#"[{"type":"while_loop","params":{}}]"#))
            .unwrap()
            .violations;
        assert_eq!(codes(&v), vec!["DC-01"], "未知元指令应命中 DC-01");
    }

    #[test]
    fn nested_enforce_is_rejected() {
        let json = r#"[{"type":"branch","params":{"domain":{"type":"exists","path":"a"},
            "on_true":[{"type":"enforce","params":{"domain":{"type":"exists","path":"b"},"reason":"r"}}],
            "on_false":[]}}]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert_eq!(codes(&v), vec!["DC-02"], "嵌套 enforce 应命中 DC-02");
    }

    #[test]
    fn shadowed_io_request_is_rejected() {
        // BUG-P0-005 同型:前面的 io_request 抢先返回信号,后面同类型的永远不求值
        let json = r#"[
          {"type":"io_request","params":{"io_type":"audit"},
           "domain":{"type":"instruction","instruction_type":"call_external"}},
          {"type":"branch","params":{"domain":{"type":"instruction","instruction_type":"call_external"},
            "on_true":[{"type":"io_request","params":{"io_type":"llm"}}],"on_false":[]}}
        ]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(
            codes(&v).contains(&"DC-05"),
            "被遮蔽的 io_request 应命中 DC-05, got {v:?}"
        );
    }

    #[test]
    fn mutually_exclusive_rules_are_not_shadow_sources() {
        // report-006 F1 / R-A4:core_eval [8]/[9] 同型 —— source 要求
        // `exists p.service_name`,owner 要求 `not(exists p.service_name)`,
        // 两者不可能同时命中 ⇒ 无遮蔽,不得误报 DC-05。
        // (B 段若在此误报上强制拒载,server 会拒载自己的 core_eval。)
        let json = r#"[
          {"type":"branch","params":{"domain":{"type":"all","inner":[
             {"type":"instruction","instruction_type":"call_service"},
             {"type":"exists","path":"p.service_name"}]},
            "on_true":[{"type":"io_request","params":{"io_type":"call_service"}}]}},
          {"type":"branch","params":{"domain":{"type":"all","inner":[
             {"type":"instruction","instruction_type":"call_service"},
             {"type":"not","inner":{"type":"exists","path":"p.service_name"}}]},
            "on_true":[{"type":"io_request","params":{"io_type":"call_service"}}]}}
        ]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(v.is_empty(), "互斥规则不得判遮蔽: {v:?}");
    }

    #[test]
    fn negated_all_does_not_create_false_exclusion() {
        // 健全性:`not(all[exists P])` 推不出 `¬exists P`——不得据此豁免遮蔽。
        // 去掉互斥精化的可证明前提后,保守遮蔽判定必须原样生效。
        let json = r#"[{"type":"branch","params":{"domain":{"type":"all","inner":[{"type":"instruction","instruction_type":"call_service"},{"type":"exists","path":"p.service_name"}]},"on_true":[{"type":"io_request","params":{"io_type":"call_service"}}]}},{"type":"branch","params":{"domain":{"type":"all","inner":[{"type":"instruction","instruction_type":"call_service"},{"type":"not","inner":{"type":"all","inner":[{"type":"exists","path":"p.service_name"}]}},{"type":"exists","path":"p.tool_name"}]},"on_true":[{"type":"io_request","params":{"io_type":"call_service"}}]}}]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(
            codes(&v).contains(&"DC-05"),
            "not(all) 嵌套不构成互斥,保守遮蔽判定应生效: {v:?}"
        );
    }

    #[test]
    fn double_io_in_one_rule_is_rejected() {
        let json = r#"[{"type":"branch","params":{"domain":{"type":"exists","path":"a"},
            "on_true":[{"type":"io_request","params":{"io_type":"llm"}}],
            "on_false":[{"type":"io_request","params":{"io_type":"db"}}]}}]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(
            codes(&v).iter().filter(|c| **c == "DC-08").count() == 2,
            "一条规则内两个 io_request 应命中 DC-08 两次, got {v:?}"
        );
    }

    #[test]
    fn io_request_after_mutation_is_rejected() {
        // TCB_SPEC D5:io_request 前的 set 会被丢弃(IoRequired 是纯信号)→ 重放失真
        let json = r#"[
          {"type":"set","params":{"attr":"x","value":1}},
          {"type":"io_request","params":{"io_type":"llm"}}
        ]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(
            codes(&v).contains(&"DC-09"),
            "io_request 前有改状态操作应命中 DC-09, got {v:?}"
        );
    }

    #[test]
    fn disjoint_top_rule_mutations_are_not_same_pass() {
        // R-B1 两段式整改的前提:前序顶层规则 [0](domain=foo)含 set,
        // 后序规则 [1](domain=bar)含 io_request。指令类型每趟唯一,
        // [0] 在 [1] 命中的那一趟必不执行 ⇒ 其 set 构不成同趟前置,
        // 不得误报 DC-09。(set→push 两段式:phase1 准备参数,phase2 纯 io。)
        let json = r#"[{"type":"branch","params":{"domain":{"type":"instruction","instruction_type":"foo"},"on_true":[{"type":"set","params":{"attr":"_args.v","value":1}}],"on_false":[]}},{"type":"branch","params":{"domain":{"type":"instruction","instruction_type":"bar"},"on_true":[{"type":"io_request","params":{"io_type":"call_service","args":"__exec__.payload._args"}}],"on_false":[]}}]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(
            v.is_empty(),
            "指令类型不相交的前序规则不构成同趟前置: {v:?}"
        );
    }

    #[test]
    fn overlapping_top_rule_mutations_still_rejected() {
        // 健全性(对上一测试的反向锁死):前序规则与 io 所属规则目标集相交
        // (或前序为 ANY 空集)时,可能同一趟执行 ⇒ set 必须照常命中 DC-09。
        let json = r#"[{"type":"branch","params":{"domain":{"type":"instruction","instruction_type":"foo"},"on_true":[{"type":"set","params":{"attr":"_args.v","value":1}}],"on_false":[]}},{"type":"branch","params":{"domain":{"type":"instruction","instruction_type":"foo"},"on_true":[{"type":"io_request","params":{"io_type":"call_service","args":"__exec__.payload._args"}}],"on_false":[]}}]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(
            codes(&v).contains(&"DC-09"),
            "目标集相交的前序 set 仍是同趟前置, got {v:?}"
        );

        let any_pred = r#"[{"type":"set","params":{"attr":"x","value":1}},{"type":"branch","params":{"domain":{"type":"instruction","instruction_type":"bar"},"on_true":[{"type":"io_request","params":{"io_type":"llm"}}],"on_false":[]}}]"#;
        let v2 = check(&ruleset(any_pred)).unwrap().violations;
        assert!(
            codes(&v2).contains(&"DC-09"),
            "无指令谓词(ANY)的前序 set 按保守口径仍命中, got {v2:?}"
        );
    }

    #[test]
    fn mutually_exclusive_top_rules_are_not_same_pass() {
        // 宪法 server_eval [8]/[9] 同型(R-B2 同步时暴露的误报):
        // [8] call_service ∧ exists service_name(含 set),[9] call_service ∧
        // not(exists service_name)(含 io)。两规则不可能同趟命中 ⇒ [8] 的 set
        // 构不成对 [9] 的 io 的同趟前置,不得误报 DC-09。
        let json = r#"[{"type":"branch","params":{"domain":{"type":"all","inner":[{"type":"instruction","instruction_type":"call_service"},{"type":"exists","path":"__exec__.instruction.params.service_name"}]},"on_true":[{"type":"set","params":{"attr":"x","value":1}}],"on_false":[]}},{"type":"branch","params":{"domain":{"type":"all","inner":[{"type":"instruction","instruction_type":"call_service"},{"type":"not","inner":{"type":"exists","path":"__exec__.instruction.params.service_name"}}]},"on_true":[{"type":"branch","params":{"domain":{"type":"exists","path":"__exec__.payload.__io_results__.call_service"},"on_false":[{"type":"io_request","params":{"io_type":"call_service"}}],"on_true":[]}}],"on_false":[]}}]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(
            v.is_empty(),
            "exists/¬exists 互斥的前序规则不构成同趟前置: {v:?}"
        );
    }

    #[test]
    fn missing_type_field_is_rejected() {
        let v = check(&ruleset(r#"[{"params":{}}]"#)).unwrap().violations;
        assert_eq!(codes(&v), vec!["DC-01"], "缺 type 字段应命中 DC-01");
    }

    /// 真实误报复盘:早期 wasm 侧演示曾用 `set value: null` 清空 I/O 结果。
    /// domain 的 `exists` 语义是「null = 已清除」,第一版 DC-06 因此误报;
    /// 本测试锁死「字段存在即可,值可为 null」的正确判据。
    #[test]
    fn set_with_null_value_is_accepted() {
        let json = r#"[{"type":"set","params":{"attr":"__io_results__.demo","operation":"set","value":null}}]"#;
        let v = check(&ruleset(json)).unwrap().violations;
        assert!(v.is_empty(), "value=null 的 set 不得被拒载: {v:?}");
    }

    #[test]
    fn set_without_value_field_is_rejected() {
        let v = check(&ruleset(r#"[{"type":"set","params":{"attr":"x"}}]"#))
            .unwrap()
            .violations;
        assert_eq!(codes(&v), vec!["DC-06"], "缺 value 字段应命中 DC-06");
    }

    /// B 段回归锁(design-06 §11):嵌入的纪律数据本身必须能被门禁通过——
    /// 纪律条款全为顶层 enforce 且带 reason(DC-02/DC-03 自身合规)。
    #[test]
    fn embedded_discipline_ruleset_is_self_compliant() {
        let report = check(&load_discipline().unwrap()).unwrap();
        assert!(
            report.violations.is_empty(),
            "纪律数据自身违规(自指失败): {:?}",
            report.violations
        );
    }
}