1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
11#[serde(rename_all = "camelCase")]
12pub enum Confidence {
13 Speculative,
15 Uncertain,
17 Medium,
19 High,
21 Certain,
23}
24
25impl Confidence {
26 pub fn as_str(&self) -> &'static str {
27 match self {
28 Confidence::Speculative => "speculative",
29 Confidence::Uncertain => "uncertain",
30 Confidence::Medium => "medium",
31 Confidence::High => "high",
32 Confidence::Certain => "certain",
33 }
34 }
35
36 pub fn from_str(s: &str) -> Option<Self> {
37 match s {
38 "speculative" => Some(Confidence::Speculative),
39 "uncertain" => Some(Confidence::Uncertain),
40 "medium" => Some(Confidence::Medium),
41 "high" => Some(Confidence::High),
42 "certain" => Some(Confidence::Certain),
43 _ => None,
44 }
45 }
46
47 pub fn factor(&self) -> f64 {
49 match self {
50 Confidence::Speculative => 0.15,
51 Confidence::Uncertain => 0.4,
52 Confidence::Medium => 0.7,
53 Confidence::High => 0.9,
54 Confidence::Certain => 1.0,
55 }
56 }
57
58 pub fn search_boost(&self) -> f64 {
60 match self {
61 Confidence::Speculative => 0.5,
62 Confidence::Uncertain => 0.8,
63 Confidence::Medium => 1.0,
64 Confidence::High => 1.2,
65 Confidence::Certain => 1.5,
66 }
67 }
68
69 pub fn confirm(&self) -> (Confidence, bool) {
71 match self {
72 Confidence::Speculative => (Confidence::Uncertain, true),
73 Confidence::Uncertain => (Confidence::Medium, true),
74 Confidence::Medium => (Confidence::High, true),
75 Confidence::High => (Confidence::Certain, true),
76 Confidence::Certain => (Confidence::Certain, false), }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct Provenance {
84 pub source: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub derived_from: Option<Vec<String>>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub confirmed_by: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub confirmed_at: Option<String>,
95 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
97 pub auto_generated: bool,
98 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
100 pub needs_review: bool,
101}
102
103impl Provenance {
104 pub fn new(source: &str) -> Self {
105 Provenance {
106 source: source.to_string(),
107 derived_from: None,
108 confirmed_by: None,
109 confirmed_at: None,
110 auto_generated: false,
111 needs_review: false,
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Dispute {
119 pub id: String,
120 pub reason: String,
121 #[serde(rename = "disputedAt")]
122 pub disputed_at: String,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 #[serde(rename = "resolvedAt")]
125 pub resolved_at: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub resolution: Option<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct AuditEntry {
133 pub action: String,
134 pub by: String,
135 pub at: String,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub detail: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 #[serde(rename = "mergedFrom")]
140 pub merged_from: Option<String>,
141}
142
143impl AuditEntry {
144 pub fn new(action: &str, by: &str, at: &str) -> Self {
145 AuditEntry {
146 action: action.to_string(),
147 by: by.to_string(),
148 at: at.to_string(),
149 detail: None,
150 merged_from: None,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct TrustData {
158 pub confidence: Confidence,
159 pub provenance: Provenance,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub disputes: Option<Vec<Dispute>>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 #[serde(rename = "auditLog")]
164 pub audit_log: Option<Vec<AuditEntry>>,
165}
166
167impl TrustData {
168 pub fn new(confidence: Confidence, source: &str) -> Self {
169 TrustData {
170 confidence,
171 provenance: Provenance::new(source),
172 disputes: None,
173 audit_log: None,
174 }
175 }
176
177 pub fn append_audit(&mut self, entry: AuditEntry) {
179 if let Some(ref mut log) = self.audit_log {
180 log.push(entry);
181 } else {
182 self.audit_log = Some(vec![entry]);
183 }
184 }
185
186 pub fn dispute_decay(&self) -> f64 {
188 match &self.disputes {
189 Some(d) => d.len() as f64 * 0.15,
190 None => 0.0,
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_confidence_ladder() {
201 let (next, changed) = Confidence::Uncertain.confirm();
202 assert_eq!(next, Confidence::Medium);
203 assert!(changed);
204
205 let (next, changed) = Confidence::Certain.confirm();
206 assert_eq!(next, Confidence::Certain);
207 assert!(!changed);
208 }
209
210 #[test]
211 fn test_confidence_factor() {
212 assert!(Confidence::Certain.factor() > Confidence::Speculative.factor());
213 assert_eq!(Confidence::Certain.factor(), 1.0);
214 assert_eq!(Confidence::Speculative.factor(), 0.15);
215 }
216
217 #[test]
218 fn test_provenance_new() {
219 let p = Provenance::new("CLI");
220 assert_eq!(p.source, "CLI");
221 assert!(!p.auto_generated);
222 }
223
224 #[test]
225 fn test_audit_append() {
226 let mut td = TrustData::new(Confidence::Uncertain, "test");
227 td.append_audit(AuditEntry::new("recorded", "CLI", "2026-01-01T00:00:00Z"));
228 assert_eq!(td.audit_log.as_ref().unwrap().len(), 1);
229 }
230
231 #[test]
232 fn test_dispute_decay() {
233 let mut td = TrustData::new(Confidence::High, "test");
234 assert_eq!(td.dispute_decay(), 0.0);
235
236 td.disputes = Some(vec![Dispute {
237 id: "d1".into(),
238 reason: "测试质疑".into(),
239 disputed_at: "2026-01-01T00:00:00Z".into(),
240 resolved_at: None,
241 resolution: None,
242 }]);
243 assert_eq!(td.dispute_decay(), 0.15);
244 }
245}