chronicle/schema/
correction.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "snake_case")]
6pub enum CorrectionType {
7 Flag,
9 Remove,
11 Amend,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Correction {
18 pub field: String,
20
21 pub correction_type: CorrectionType,
23
24 pub reason: String,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub target_value: Option<String>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub replacement: Option<String>,
34
35 pub timestamp: String,
37
38 pub author: String,
40}
41
42pub const CORRECTION_PENALTY: f64 = 0.15;
44
45pub const CORRECTION_FLOOR: f64 = 0.1;
47
48pub fn apply_correction_penalty(base_confidence: f64, correction_count: usize) -> f64 {
50 let penalty = correction_count as f64 * CORRECTION_PENALTY;
51 (base_confidence - penalty).max(CORRECTION_FLOOR)
52}
53
54pub fn resolve_author(git: &dyn crate::git::GitOps) -> String {
56 if let Ok(session) = std::env::var("CHRONICLE_SESSION") {
58 if !session.is_empty() {
59 return session;
60 }
61 }
62
63 let name = git
65 .config_get("user.name")
66 .ok()
67 .flatten()
68 .unwrap_or_default();
69 let email = git
70 .config_get("user.email")
71 .ok()
72 .flatten()
73 .unwrap_or_default();
74
75 if !name.is_empty() && !email.is_empty() {
76 format!("{name} <{email}>")
77 } else if !name.is_empty() {
78 name
79 } else if !email.is_empty() {
80 email
81 } else {
82 "unknown".to_string()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_correction_roundtrip() {
92 let correction = Correction {
93 field: "constraints".to_string(),
94 correction_type: CorrectionType::Remove,
95 reason: "No longer required since v2.3".to_string(),
96 target_value: Some("Must drain queue".to_string()),
97 replacement: None,
98 timestamp: "2025-12-20T14:30:00Z".to_string(),
99 author: "test-user".to_string(),
100 };
101
102 let json = serde_json::to_string(&correction).unwrap();
103 let parsed: Correction = serde_json::from_str(&json).unwrap();
104
105 assert_eq!(parsed.field, "constraints");
106 assert_eq!(parsed.correction_type, CorrectionType::Remove);
107 assert_eq!(parsed.reason, "No longer required since v2.3");
108 assert_eq!(parsed.target_value.as_deref(), Some("Must drain queue"));
109 assert!(parsed.replacement.is_none());
110 assert_eq!(parsed.author, "test-user");
111 }
112
113 #[test]
114 fn test_correction_type_serialization() {
115 assert_eq!(
116 serde_json::to_string(&CorrectionType::Flag).unwrap(),
117 "\"flag\""
118 );
119 assert_eq!(
120 serde_json::to_string(&CorrectionType::Remove).unwrap(),
121 "\"remove\""
122 );
123 assert_eq!(
124 serde_json::to_string(&CorrectionType::Amend).unwrap(),
125 "\"amend\""
126 );
127 }
128
129 #[test]
130 fn test_correction_type_deserialization() {
131 let flag: CorrectionType = serde_json::from_str("\"flag\"").unwrap();
132 assert_eq!(flag, CorrectionType::Flag);
133 let remove: CorrectionType = serde_json::from_str("\"remove\"").unwrap();
134 assert_eq!(remove, CorrectionType::Remove);
135 let amend: CorrectionType = serde_json::from_str("\"amend\"").unwrap();
136 assert_eq!(amend, CorrectionType::Amend);
137 }
138
139 #[test]
140 fn test_apply_correction_penalty() {
141 assert_eq!(apply_correction_penalty(0.85, 0), 0.85);
142 assert_eq!(apply_correction_penalty(0.85, 1), 0.7);
143 assert_eq!(apply_correction_penalty(0.85, 2), 0.55);
144 assert_eq!(apply_correction_penalty(0.85, 10), CORRECTION_FLOOR);
146 assert_eq!(apply_correction_penalty(0.3, 2), CORRECTION_FLOOR);
147 }
148
149 #[test]
150 fn test_flag_correction_no_target_value() {
151 let correction = Correction {
152 field: "intent".to_string(),
153 correction_type: CorrectionType::Flag,
154 reason: "Annotation seems wrong".to_string(),
155 target_value: None,
156 replacement: None,
157 timestamp: "2025-12-20T14:30:00Z".to_string(),
158 author: "tester".to_string(),
159 };
160
161 let json = serde_json::to_string(&correction).unwrap();
162 assert!(!json.contains("target_value"));
164 assert!(!json.contains("replacement"));
165 }
166
167 #[test]
168 fn test_amend_correction_with_replacement() {
169 let correction = Correction {
170 field: "reasoning".to_string(),
171 correction_type: CorrectionType::Amend,
172 reason: "Updated reasoning".to_string(),
173 target_value: None,
174 replacement: Some("New reasoning text".to_string()),
175 timestamp: "2025-12-20T14:30:00Z".to_string(),
176 author: "tester".to_string(),
177 };
178
179 let json = serde_json::to_string(&correction).unwrap();
180 let parsed: Correction = serde_json::from_str(&json).unwrap();
181 assert_eq!(parsed.correction_type, CorrectionType::Amend);
182 assert_eq!(parsed.replacement.as_deref(), Some("New reasoning text"));
183 }
184}