1use crate::runtime::mesh_types::{
2 FailureMode, Hook, HookDescriptor, HookEvent, InstanceId, MaxDiagnostic, MeshAction,
3 PolicyState, Receipt,
4};
5
6pub 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}