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
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Configuration for the MARCH self-check quality pipeline.
//!
//! Add a `[quality]` section to `config.toml`:
//!
//! ```toml
//! [quality]
//! self_check = true
//! trigger = "has_retrieval"
//! async_run = false
//! ```
use serde::{Deserialize, Serialize};
/// When to trigger the self-check pipeline.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TriggerPolicy {
/// Run only when the turn has retrieved context.
#[default]
HasRetrieval,
/// Always run regardless of retrieved context.
Always,
/// Never run automatically.
Manual,
}
/// Configuration for the MARCH self-check quality pipeline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityConfig {
/// Enable post-response self-check pipeline.
#[serde(default)]
pub self_check: bool,
/// Advisory: preferred provider for the Proposer role (MVP: no-op).
#[serde(default)]
pub proposer_provider: String,
/// Advisory: preferred provider for the Checker role (MVP: no-op).
#[serde(default)]
pub checker_provider: String,
/// When to trigger the pipeline.
#[serde(default)]
pub trigger: TriggerPolicy,
/// Minimum evidence strength to avoid flagging an assertion (0.0–1.0).
#[serde(default = "default_min_evidence")]
pub min_evidence: f32,
/// If `false` (default), pipeline blocks response until done.
#[serde(default)]
pub async_run: bool,
/// Hard ceiling on total pipeline latency in milliseconds.
#[serde(default = "default_latency_budget_ms")]
pub latency_budget_ms: u64,
/// Per-LLM-call timeout in milliseconds.
#[serde(default = "default_per_call_timeout_ms")]
pub per_call_timeout_ms: u64,
/// Maximum assertions to extract from one response.
#[serde(default = "default_max_assertions")]
pub max_assertions: usize,
/// Skip pipeline when response exceeds this many characters.
#[serde(default = "default_max_response_chars")]
pub max_response_chars: usize,
/// Suppress prompt-cache emission on Checker provider.
#[serde(default = "default_cache_disabled_for_checker")]
pub cache_disabled_for_checker: bool,
/// Marker appended to response when issues are flagged.
#[serde(default = "default_flag_marker")]
pub flag_marker: String,
}
fn default_min_evidence() -> f32 {
0.6
}
fn default_latency_budget_ms() -> u64 {
4_000
}
fn default_per_call_timeout_ms() -> u64 {
2_000
}
fn default_max_assertions() -> usize {
12
}
fn default_max_response_chars() -> usize {
8_000
}
fn default_cache_disabled_for_checker() -> bool {
true
}
fn default_flag_marker() -> String {
"[verify]".into()
}
impl Default for QualityConfig {
fn default() -> Self {
Self {
self_check: false,
proposer_provider: String::new(),
checker_provider: String::new(),
trigger: TriggerPolicy::default(),
min_evidence: default_min_evidence(),
async_run: false,
latency_budget_ms: default_latency_budget_ms(),
per_call_timeout_ms: default_per_call_timeout_ms(),
max_assertions: default_max_assertions(),
max_response_chars: default_max_response_chars(),
cache_disabled_for_checker: default_cache_disabled_for_checker(),
flag_marker: default_flag_marker(),
}
}
}