Skip to main content

lsp_max/runtime/
mesh_hooks.rs

1use crate::runtime::mesh_types::{
2    FailureMode, Hook, HookDescriptor, HookEvent, InstanceId, MaxDiagnostic, MeshAction,
3    PolicyState, Receipt,
4};
5
6/// Enforces three process-level laws by monitoring the OCEL of the LSP session.
7/// Text scanning detects what code looks like. This hook detects what process produced it.
8/// PROCESS-001: receipt emitted while unresolved diagnostics are active
9/// PROCESS-002: receipt emitted while instance is in ClarificationRequested
10/// PROCESS-003: diagnostic cleared with no intervening resolution event (oracle injection signal)
11pub struct OcelProcessHook {
12    active_diagnostics:
13        std::sync::Mutex<std::collections::HashMap<String, std::collections::HashSet<String>>>,
14    policy_states: std::sync::Mutex<std::collections::HashMap<String, PolicyState>>,
15    diag_emission_baselines: std::sync::Mutex<std::collections::HashMap<String, u64>>,
16    resolution_counts: std::sync::Mutex<std::collections::HashMap<String, u64>>,
17}
18
19impl Default for OcelProcessHook {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl OcelProcessHook {
26    pub fn new() -> Self {
27        Self {
28            active_diagnostics: std::sync::Mutex::new(std::collections::HashMap::new()),
29            policy_states: std::sync::Mutex::new(std::collections::HashMap::new()),
30            diag_emission_baselines: std::sync::Mutex::new(std::collections::HashMap::new()),
31            resolution_counts: std::sync::Mutex::new(std::collections::HashMap::new()),
32        }
33    }
34
35    fn make_diag(
36        law_id: &str,
37        id: &str,
38        msg: &str,
39        sev: lsp_types_max::DiagnosticSeverity,
40    ) -> Box<MaxDiagnostic> {
41        Box::new(MaxDiagnostic {
42            lsp: lsp_types_max::Diagnostic {
43                range: lsp_types_max::Range::default(),
44                severity: Some(sev),
45                code: Some(lsp_types_max::NumberOrString::String(law_id.to_string())),
46                message: msg.to_string(),
47                ..Default::default()
48            },
49            diagnostic_id: id.to_string(),
50            law_id: law_id.to_string(),
51            violated_invariant: msg.to_string(),
52            ..Default::default()
53        })
54    }
55}
56
57impl Hook for OcelProcessHook {
58    fn name(&self) -> &str {
59        "OcelProcessHook"
60    }
61
62    fn trigger(&self, event: &HookEvent) -> Vec<MeshAction> {
63        let mut actions = Vec::new();
64        match event {
65            HookEvent::DiagnosticEmitted {
66                instance_id,
67                diagnostic,
68            } => {
69                if let Ok(mut d) = self.active_diagnostics.lock() {
70                    d.entry(instance_id.0.clone())
71                        .or_default()
72                        .insert(diagnostic.diagnostic_id.clone());
73                }
74                let baseline = self
75                    .resolution_counts
76                    .lock()
77                    .ok()
78                    .and_then(|m| m.get(&instance_id.0).copied())
79                    .unwrap_or(0);
80                if let Ok(mut b) = self.diag_emission_baselines.lock() {
81                    b.insert(diagnostic.diagnostic_id.clone(), baseline);
82                }
83            }
84            HookEvent::DiagnosticCleared {
85                instance_id,
86                diagnostic_id,
87            } => {
88                let baseline = self
89                    .diag_emission_baselines
90                    .lock()
91                    .ok()
92                    .and_then(|m| m.get(diagnostic_id.as_str()).copied());
93                let current = self
94                    .resolution_counts
95                    .lock()
96                    .ok()
97                    .and_then(|m| m.get(&instance_id.0).copied())
98                    .unwrap_or(0);
99                if let Some(b) = baseline {
100                    if current == b {
101                        actions.push(MeshAction::AddDiagnostic {
102                            instance_id: instance_id.clone(),
103                            diagnostic: Self::make_diag(
104                                "PROCESS-003",
105                                &format!("process-003-{}", diagnostic_id),
106                                &format!("Process violation (PROCESS-003): '{}' cleared without resolution event — oracle injection signal. Text scanning cannot detect this.", diagnostic_id),
107                                lsp_types_max::DiagnosticSeverity::WARNING,
108                            ),
109                        });
110                    }
111                }
112                if let Ok(mut d) = self.active_diagnostics.lock() {
113                    if let Some(s) = d.get_mut(&instance_id.0) {
114                        s.remove(diagnostic_id.as_str());
115                    }
116                }
117                if let Ok(mut b) = self.diag_emission_baselines.lock() {
118                    b.remove(diagnostic_id.as_str());
119                }
120            }
121            HookEvent::ReceiptEmitted {
122                instance_id,
123                receipt,
124            } => {
125                let has_active = self
126                    .active_diagnostics
127                    .lock()
128                    .ok()
129                    .and_then(|m| m.get(&instance_id.0).map(|s| !s.is_empty()))
130                    .unwrap_or(false);
131                if has_active {
132                    actions.push(MeshAction::AddDiagnostic {
133                        instance_id: instance_id.clone(),
134                        diagnostic: Self::make_diag(
135                            "PROCESS-001",
136                            &format!("process-001-{}", receipt.receipt_id),
137                            &format!("Process violation (PROCESS-001): receipt '{}' emitted while violations active — stage not lawfully complete. Text scanning cannot detect this.", receipt.receipt_id),
138                            lsp_types_max::DiagnosticSeverity::ERROR,
139                        ),
140                    });
141                }
142                let in_clarification = self
143                    .policy_states
144                    .lock()
145                    .ok()
146                    .and_then(|m| m.get(&instance_id.0).cloned())
147                    .map(|s| s == PolicyState::ClarificationRequested)
148                    .unwrap_or(false);
149                if in_clarification {
150                    actions.push(MeshAction::AddDiagnostic {
151                        instance_id: instance_id.clone(),
152                        diagnostic: Self::make_diag(
153                            "PROCESS-002",
154                            &format!("process-002-{}", receipt.receipt_id),
155                            &format!("Process violation (PROCESS-002): receipt '{}' emitted while ClarificationRequested — pending clarification not resolved. Text scanning cannot detect this.", receipt.receipt_id),
156                            lsp_types_max::DiagnosticSeverity::ERROR,
157                        ),
158                    });
159                }
160            }
161            HookEvent::PolicyStateChanged {
162                instance_id,
163                to_state,
164                ..
165            } => {
166                if let Ok(mut s) = self.policy_states.lock() {
167                    s.insert(instance_id.0.clone(), to_state.clone());
168                }
169                if let Ok(mut c) = self.resolution_counts.lock() {
170                    *c.entry(instance_id.0.clone()).or_insert(0) += 1;
171                }
172            }
173            HookEvent::BoundedActionExecuted { instance_id, .. } => {
174                if let Ok(mut c) = self.resolution_counts.lock() {
175                    *c.entry(instance_id.0.clone()).or_insert(0) += 1;
176                }
177            }
178            HookEvent::InstanceReset { instance_id } => {
179                if let Ok(mut d) = self.active_diagnostics.lock() {
180                    d.remove(&instance_id.0);
181                }
182                if let Ok(mut s) = self.policy_states.lock() {
183                    s.remove(&instance_id.0);
184                }
185                if let Ok(mut c) = self.resolution_counts.lock() {
186                    c.remove(&instance_id.0);
187                }
188            }
189            _ => {}
190        }
191        actions
192    }
193
194    fn descriptor(&self) -> HookDescriptor {
195        HookDescriptor {
196            name: "OcelProcessHook",
197            input_type: "HookEvent::DiagnosticEmitted, HookEvent::DiagnosticCleared, HookEvent::ReceiptEmitted, HookEvent::PolicyStateChanged, HookEvent::BoundedActionExecuted, HookEvent::InstanceReset",
198            output_type: "MeshAction::AddDiagnostic",
199            trigger_law: "PROCESS-001, PROCESS-002, PROCESS-003",
200            failure_mode: FailureMode::EmitDiagnostic,
201        }
202    }
203}
204
205pub struct IntakeDiagnosticHook;
206
207impl Hook for IntakeDiagnosticHook {
208    fn name(&self) -> &str {
209        "IntakeDiagnosticHook"
210    }
211
212    fn trigger(&self, event: &HookEvent) -> Vec<MeshAction> {
213        match event {
214            HookEvent::DiagnosticEmitted {
215                instance_id,
216                diagnostic,
217            } => {
218                if instance_id.0 == "LSP_1" && diagnostic.law_id == "law-intake-validation" {
219                    vec![MeshAction::TransitionPolicyState {
220                        instance_id: InstanceId::from("LSP_2"),
221                        new_state: PolicyState::ClarificationRequested,
222                    }]
223                } else {
224                    vec![]
225                }
226            }
227            _ => vec![],
228        }
229    }
230
231    fn descriptor(&self) -> HookDescriptor {
232        HookDescriptor {
233            name: "IntakeDiagnosticHook",
234            input_type: "HookEvent::DiagnosticEmitted",
235            output_type: "MeshAction::TransitionPolicyState",
236            trigger_law: "LAW-INTAKE-001",
237            failure_mode: FailureMode::EmitDiagnostic,
238        }
239    }
240}
241
242pub struct IntakeClearHook;
243
244impl Hook for IntakeClearHook {
245    fn name(&self) -> &str {
246        "IntakeClearHook"
247    }
248
249    fn trigger(&self, event: &HookEvent) -> Vec<MeshAction> {
250        match event {
251            HookEvent::DiagnosticCleared {
252                instance_id,
253                diagnostic_id,
254            } => {
255                if instance_id.0 == "LSP_1" && diagnostic_id == "diag-invalid-input" {
256                    vec![
257                        MeshAction::EmitReceipt {
258                            instance_id: InstanceId::from("LSP_1"),
259                            receipt: Receipt {
260                                receipt_id: "rcpt-intake-validated".to_string(),
261                                hash: "hash-intake-validated-mock".to_string(),
262                                prev_receipt_hash: None,
263                            },
264                        },
265                        MeshAction::TransitionPolicyState {
266                            instance_id: InstanceId::from("LSP_2"),
267                            new_state: PolicyState::RefundAuthorized,
268                        },
269                        MeshAction::ExecuteBoundedAction {
270                            instance_id: InstanceId::from("LSP_2"),
271                            action_id: "act-create-refund-receipt".to_string(),
272                            description: "Creating refund receipt file for policy execution"
273                                .to_string(),
274                        },
275                        MeshAction::EmitReceipt {
276                            instance_id: InstanceId::from("LSP_2"),
277                            receipt: Receipt {
278                                receipt_id: "rcpt-refund-executed".to_string(),
279                                hash: "hash-refund-executed-mock".to_string(),
280                                prev_receipt_hash: None,
281                            },
282                        },
283                    ]
284                } else {
285                    vec![]
286                }
287            }
288            _ => vec![],
289        }
290    }
291
292    fn descriptor(&self) -> HookDescriptor {
293        HookDescriptor {
294            name: "IntakeClearHook",
295            input_type: "HookEvent::DiagnosticCleared",
296            output_type: "MeshAction::EmitReceipt, MeshAction::TransitionPolicyState, MeshAction::ExecuteBoundedAction",
297            trigger_law: "LAW-INTAKE-002",
298            failure_mode: FailureMode::EmitDiagnostic,
299        }
300    }
301}
302
303pub struct CustomerRequestClassifierHook {
304    proof_received: std::sync::Mutex<std::collections::HashSet<String>>,
305    policy_states: std::sync::Mutex<std::collections::HashMap<String, PolicyState>>,
306}
307
308impl Default for CustomerRequestClassifierHook {
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314impl CustomerRequestClassifierHook {
315    pub fn new() -> Self {
316        Self {
317            proof_received: std::sync::Mutex::new(std::collections::HashSet::new()),
318            policy_states: std::sync::Mutex::new(std::collections::HashMap::new()),
319        }
320    }
321}
322
323impl Hook for CustomerRequestClassifierHook {
324    fn name(&self) -> &str {
325        "CustomerRequestClassifierHook"
326    }
327
328    fn trigger(&self, event: &HookEvent) -> Vec<MeshAction> {
329        let mut actions = Vec::new();
330        match event {
331            HookEvent::ReceiptEmitted {
332                instance_id,
333                receipt,
334            } if receipt.receipt_id.contains("proof")
335                || receipt.receipt_id.contains("customer-proof") =>
336            {
337                if let Ok(mut proof) = self.proof_received.lock() {
338                    proof.insert(instance_id.0.clone());
339                }
340            }
341            HookEvent::PolicyStateChanged {
342                instance_id,
343                from_state: _,
344                to_state,
345            } => {
346                if let Ok(mut states) = self.policy_states.lock() {
347                    states.insert(instance_id.0.clone(), to_state.clone());
348                }
349            }
350            HookEvent::DiagnosticEmitted {
351                instance_id,
352                diagnostic,
353            } => {
354                let diag_id = &diagnostic.diagnostic_id;
355                let message = diagnostic.lsp.message.to_lowercase();
356                let is_proof_issue = diag_id == "missing-proof"
357                    || diag_id == "damaged-proof"
358                    || message.contains("proof is missing")
359                    || message.contains("proof is damaged")
360                    || message.contains("damaged proof")
361                    || message.contains("missing proof");
362                if is_proof_issue {
363                    let should_transition = if let Ok(states) = self.policy_states.lock() {
364                        !matches!(
365                            states.get(instance_id.0.as_str()),
366                            Some(PolicyState::ClarificationRequested)
367                                | Some(PolicyState::RefundAuthorized)
368                        )
369                    } else {
370                        true
371                    };
372                    if should_transition {
373                        actions.push(MeshAction::TransitionPolicyState {
374                            instance_id: instance_id.clone(),
375                            new_state: PolicyState::ClarificationRequested,
376                        });
377                    }
378                }
379            }
380            HookEvent::StateTransition {
381                instance_id,
382                from_phase: _,
383                to_phase,
384            } if to_phase == "Initialized" => {
385                let is_missing = if let Ok(proof) = self.proof_received.lock() {
386                    !proof.contains(instance_id.0.as_str())
387                } else {
388                    true
389                };
390                if is_missing {
391                    let should_transition = if let Ok(states) = self.policy_states.lock() {
392                        !matches!(
393                            states.get(instance_id.0.as_str()),
394                            Some(PolicyState::ClarificationRequested)
395                                | Some(PolicyState::RefundAuthorized)
396                        )
397                    } else {
398                        true
399                    };
400                    if should_transition {
401                        actions.push(MeshAction::TransitionPolicyState {
402                            instance_id: instance_id.clone(),
403                            new_state: PolicyState::ClarificationRequested,
404                        });
405                    }
406                }
407            }
408            HookEvent::BoundedActionExecuted {
409                instance_id,
410                action_id,
411                description,
412            } => {
413                if let Ok(mut proof) = self.proof_received.lock() {
414                    proof.insert(instance_id.0.clone());
415                }
416                actions.push(MeshAction::EmitReceipt {
417                    instance_id: instance_id.clone(),
418                    receipt: Receipt {
419                        receipt_id: format!("bounded-action-executed-{}", action_id),
420                        hash: format!("sha256:bounded:{}:{}", action_id, description.len()),
421                        prev_receipt_hash: None,
422                    },
423                });
424            }
425            HookEvent::InstanceReset { instance_id } => {
426                if let Ok(mut proof) = self.proof_received.lock() {
427                    proof.remove(&instance_id.0);
428                }
429                if let Ok(mut states) = self.policy_states.lock() {
430                    states.remove(&instance_id.0);
431                }
432            }
433            _ => {}
434        }
435        actions
436    }
437
438    fn descriptor(&self) -> HookDescriptor {
439        HookDescriptor {
440            name: "CustomerRequestClassifierHook",
441            input_type: "HookEvent::ReceiptEmitted, HookEvent::PolicyStateChanged, HookEvent::DiagnosticEmitted, HookEvent::StateTransition, HookEvent::BoundedActionExecuted, HookEvent::InstanceReset",
442            output_type: "MeshAction::TransitionPolicyState, MeshAction::EmitReceipt",
443            trigger_law: "LAW-CLASSIFY-001",
444            failure_mode: FailureMode::RefuseEvent,
445        }
446    }
447}
448
449pub struct PolicyEvaluationHook {
450    policy_states: std::sync::Mutex<std::collections::HashMap<String, PolicyState>>,
451}
452
453impl Default for PolicyEvaluationHook {
454    fn default() -> Self {
455        Self::new()
456    }
457}
458
459impl PolicyEvaluationHook {
460    pub fn new() -> Self {
461        Self {
462            policy_states: std::sync::Mutex::new(std::collections::HashMap::new()),
463        }
464    }
465}
466
467impl Hook for PolicyEvaluationHook {
468    fn name(&self) -> &str {
469        "PolicyEvaluationHook"
470    }
471
472    fn trigger(&self, event: &HookEvent) -> Vec<MeshAction> {
473        let mut actions = Vec::new();
474        match event {
475            HookEvent::ReceiptEmitted {
476                instance_id,
477                receipt,
478            } if receipt.receipt_id.contains("proof")
479                || receipt.receipt_id.contains("customer-proof") =>
480            {
481                let is_clarification_requested = if let Ok(states) = self.policy_states.lock() {
482                    states.get(&instance_id.0) == Some(&PolicyState::ClarificationRequested)
483                } else {
484                    false
485                };
486                if is_clarification_requested {
487                    actions.push(MeshAction::TransitionPolicyState {
488                        instance_id: instance_id.clone(),
489                        new_state: PolicyState::RefundAuthorized,
490                    });
491                }
492            }
493            HookEvent::PolicyStateChanged {
494                instance_id,
495                from_state,
496                to_state,
497            } => {
498                if let Ok(mut states) = self.policy_states.lock() {
499                    states.insert(instance_id.0.clone(), to_state.clone());
500                }
501                if from_state == &PolicyState::ClarificationRequested
502                    && to_state == &PolicyState::RefundAuthorized
503                {
504                    actions.push(MeshAction::ExecuteBoundedAction {
505                        instance_id: instance_id.clone(),
506                        action_id: "act-create-refund-receipt".to_string(),
507                        description: "Arrival of proof validated, creating refund receipt"
508                            .to_string(),
509                    });
510                }
511            }
512            HookEvent::BoundedActionExecuted {
513                instance_id,
514                action_id,
515                ..
516            } if action_id == "act-create-refund-receipt" => {
517                actions.push(MeshAction::EmitReceipt {
518                    instance_id: instance_id.clone(),
519                    receipt: Receipt {
520                        receipt_id: "refund-action-completion-receipt".to_string(),
521                        hash: format!("sha256:completion:{}", action_id),
522                        prev_receipt_hash: None,
523                    },
524                });
525            }
526            HookEvent::InstanceReset { instance_id } => {
527                if let Ok(mut states) = self.policy_states.lock() {
528                    states.remove(&instance_id.0);
529                }
530            }
531            _ => {}
532        }
533        actions
534    }
535
536    fn descriptor(&self) -> HookDescriptor {
537        HookDescriptor {
538            name: "PolicyEvaluationHook",
539            input_type: "HookEvent::ReceiptEmitted, HookEvent::PolicyStateChanged, HookEvent::BoundedActionExecuted, HookEvent::InstanceReset",
540            output_type: "MeshAction::TransitionPolicyState, MeshAction::ExecuteBoundedAction, MeshAction::EmitReceipt",
541            trigger_law: "LAW-POLICY-001",
542            failure_mode: FailureMode::Halt,
543        }
544    }
545}
546
547pub struct ReceiptRoutingHook {
548    active_diagnostics:
549        std::sync::Mutex<std::collections::HashMap<String, std::collections::HashSet<String>>>,
550}
551
552impl Default for ReceiptRoutingHook {
553    fn default() -> Self {
554        Self::new()
555    }
556}
557
558impl ReceiptRoutingHook {
559    pub fn new() -> Self {
560        Self {
561            active_diagnostics: std::sync::Mutex::new(std::collections::HashMap::new()),
562        }
563    }
564}
565
566impl Hook for ReceiptRoutingHook {
567    fn name(&self) -> &str {
568        "ReceiptRoutingHook"
569    }
570
571    fn trigger(&self, event: &HookEvent) -> Vec<MeshAction> {
572        let mut actions = Vec::new();
573        match event {
574            HookEvent::DiagnosticEmitted {
575                instance_id,
576                diagnostic,
577            } => {
578                if let Ok(mut diags) = self.active_diagnostics.lock() {
579                    diags
580                        .entry(instance_id.0.clone())
581                        .or_default()
582                        .insert(diagnostic.diagnostic_id.clone());
583                }
584            }
585            HookEvent::DiagnosticCleared {
586                instance_id,
587                diagnostic_id,
588            } => {
589                if let Ok(mut diags) = self.active_diagnostics.lock() {
590                    if let Some(set) = diags.get_mut(&instance_id.0) {
591                        set.remove(diagnostic_id);
592                    }
593                }
594            }
595            HookEvent::ReceiptEmitted {
596                instance_id,
597                receipt: _,
598            } => {
599                let target_instance = if instance_id.0 == "LSP_2" {
600                    Some("LSP_1".to_string())
601                } else if instance_id.0.contains("LSP_2") {
602                    Some(instance_id.0.replace("LSP_2", "LSP_1"))
603                } else if instance_id.0.contains("lsp_2") {
604                    Some(instance_id.0.replace("lsp_2", "lsp_1"))
605                } else {
606                    None
607                };
608
609                if let Some(target) = target_instance {
610                    if let Ok(diags) = self.active_diagnostics.lock() {
611                        if let Some(set) = diags.get(&target) {
612                            for diag_id in set {
613                                actions.push(MeshAction::ClearDiagnostic {
614                                    instance_id: InstanceId::from(target.clone()),
615                                    diagnostic_id: diag_id.clone(),
616                                });
617                            }
618                        }
619                    }
620                }
621            }
622            _ => {}
623        }
624        actions
625    }
626
627    fn descriptor(&self) -> HookDescriptor {
628        HookDescriptor {
629            name: "ReceiptRoutingHook",
630            input_type: "HookEvent::DiagnosticEmitted, HookEvent::DiagnosticCleared, HookEvent::ReceiptEmitted",
631            output_type: "MeshAction::ClearDiagnostic",
632            trigger_law: "LAW-ROUTING-001",
633            failure_mode: FailureMode::EmitDiagnostic,
634        }
635    }
636}
637
638#[cfg(test)]
639mod ocel_process_hook_tests {
640    use super::*;
641
642    fn diag(id: &str) -> Box<crate::runtime::mesh_types::MaxDiagnostic> {
643        Box::new(crate::runtime::mesh_types::MaxDiagnostic {
644            diagnostic_id: id.to_string(),
645            law_id: "COG-001".to_string(),
646            ..Default::default()
647        })
648    }
649    fn rcpt(id: &str) -> Receipt {
650        Receipt {
651            receipt_id: id.to_string(),
652            hash: "h".to_string(),
653            prev_receipt_hash: None,
654        }
655    }
656    fn inst(id: &str) -> InstanceId {
657        InstanceId::from(id)
658    }
659
660    #[test]
661    fn process_001_fires_with_active_diagnostic() {
662        let h = OcelProcessHook::new();
663        h.trigger(&HookEvent::DiagnosticEmitted {
664            instance_id: inst("A"),
665            diagnostic: diag("d1"),
666        });
667        let acts = h.trigger(&HookEvent::ReceiptEmitted {
668            instance_id: inst("A"),
669            receipt: rcpt("r1"),
670        });
671        assert!(acts
672            .iter()
673            .any(|a| matches!(a, MeshAction::AddDiagnostic { diagnostic, .. } if diagnostic.law_id == "PROCESS-001")));
674    }
675
676    #[test]
677    fn process_001_silent_when_no_active_diagnostics() {
678        let h = OcelProcessHook::new();
679        let acts = h.trigger(&HookEvent::ReceiptEmitted {
680            instance_id: inst("A"),
681            receipt: rcpt("r1"),
682        });
683        assert!(acts.is_empty());
684    }
685
686    #[test]
687    fn process_002_fires_in_clarification_requested_state() {
688        let h = OcelProcessHook::new();
689        h.trigger(&HookEvent::PolicyStateChanged {
690            instance_id: inst("B"),
691            from_state: PolicyState::Operational,
692            to_state: PolicyState::ClarificationRequested,
693        });
694        let acts = h.trigger(&HookEvent::ReceiptEmitted {
695            instance_id: inst("B"),
696            receipt: rcpt("r2"),
697        });
698        assert!(acts
699            .iter()
700            .any(|a| matches!(a, MeshAction::AddDiagnostic { diagnostic, .. } if diagnostic.law_id == "PROCESS-002")));
701    }
702
703    #[test]
704    fn process_003_fires_on_forced_clear() {
705        let h = OcelProcessHook::new();
706        h.trigger(&HookEvent::DiagnosticEmitted {
707            instance_id: inst("A"),
708            diagnostic: diag("d-oracle"),
709        });
710        let acts = h.trigger(&HookEvent::DiagnosticCleared {
711            instance_id: inst("A"),
712            diagnostic_id: "d-oracle".to_string(),
713        });
714        assert!(acts
715            .iter()
716            .any(|a| matches!(a, MeshAction::AddDiagnostic { diagnostic, .. } if diagnostic.law_id == "PROCESS-003")));
717    }
718
719    #[test]
720    fn process_003_silent_after_resolution_event() {
721        let h = OcelProcessHook::new();
722        h.trigger(&HookEvent::DiagnosticEmitted {
723            instance_id: inst("A"),
724            diagnostic: diag("d-legit"),
725        });
726        h.trigger(&HookEvent::BoundedActionExecuted {
727            instance_id: inst("A"),
728            action_id: "fix".to_string(),
729            description: "Applied repair".to_string(),
730        });
731        let acts = h.trigger(&HookEvent::DiagnosticCleared {
732            instance_id: inst("A"),
733            diagnostic_id: "d-legit".to_string(),
734        });
735        assert!(!acts
736            .iter()
737            .any(|a| matches!(a, MeshAction::AddDiagnostic { diagnostic, .. } if diagnostic.law_id == "PROCESS-003")));
738    }
739}