agentic_codebase/workspace/
translation.rs1#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum TranslationStatus {
15 NotStarted,
17 InProgress,
19 Ported,
21 Verified,
23 Skipped,
25}
26
27impl TranslationStatus {
28 pub fn from_str(s: &str) -> Option<Self> {
33 match s.to_lowercase().replace('-', "_").as_str() {
34 "not_started" => Some(Self::NotStarted),
35 "in_progress" => Some(Self::InProgress),
36 "ported" => Some(Self::Ported),
37 "verified" => Some(Self::Verified),
38 "skipped" => Some(Self::Skipped),
39 _ => None,
40 }
41 }
42
43 pub fn label(&self) -> &str {
45 match self {
46 Self::NotStarted => "not_started",
47 Self::InProgress => "in_progress",
48 Self::Ported => "ported",
49 Self::Verified => "verified",
50 Self::Skipped => "skipped",
51 }
52 }
53
54 fn is_complete(&self) -> bool {
56 matches!(self, Self::Ported | Self::Verified | Self::Skipped)
57 }
58}
59
60impl std::fmt::Display for TranslationStatus {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(self.label())
63 }
64}
65
66#[derive(Debug)]
72pub struct TranslationMapping {
73 pub source_symbol: String,
75 pub target_symbol: Option<String>,
77 pub status: TranslationStatus,
79 pub notes: Option<String>,
81}
82
83#[derive(Debug, Clone)]
89pub struct TranslationProgress {
90 pub total: usize,
92 pub not_started: usize,
94 pub in_progress: usize,
96 pub ported: usize,
98 pub verified: usize,
100 pub skipped: usize,
102 pub percent_complete: f32,
106}
107
108#[derive(Debug)]
118pub struct TranslationMap {
119 pub source_context: String,
121 pub target_context: String,
123 mappings: Vec<TranslationMapping>,
125}
126
127impl TranslationMap {
128 pub fn new(source_context: String, target_context: String) -> Self {
130 Self {
131 source_context,
132 target_context,
133 mappings: Vec::new(),
134 }
135 }
136
137 pub fn record(
142 &mut self,
143 source: &str,
144 target: Option<&str>,
145 status: TranslationStatus,
146 notes: Option<String>,
147 ) {
148 if let Some(existing) = self.mappings.iter_mut().find(|m| m.source_symbol == source) {
150 existing.target_symbol = target.map(|s| s.to_string());
151 existing.status = status;
152 existing.notes = notes;
153 return;
154 }
155
156 self.mappings.push(TranslationMapping {
157 source_symbol: source.to_string(),
158 target_symbol: target.map(|s| s.to_string()),
159 status,
160 notes,
161 });
162 }
163
164 pub fn status(&self, source: &str) -> Option<&TranslationMapping> {
166 self.mappings.iter().find(|m| m.source_symbol == source)
167 }
168
169 pub fn progress(&self) -> TranslationProgress {
171 let total = self.mappings.len();
172 let mut not_started = 0usize;
173 let mut in_progress = 0usize;
174 let mut ported = 0usize;
175 let mut verified = 0usize;
176 let mut skipped = 0usize;
177
178 for m in &self.mappings {
179 match m.status {
180 TranslationStatus::NotStarted => not_started += 1,
181 TranslationStatus::InProgress => in_progress += 1,
182 TranslationStatus::Ported => ported += 1,
183 TranslationStatus::Verified => verified += 1,
184 TranslationStatus::Skipped => skipped += 1,
185 }
186 }
187
188 let percent_complete = if total > 0 {
189 (ported + verified + skipped) as f32 / total as f32 * 100.0
190 } else {
191 0.0
192 };
193
194 TranslationProgress {
195 total,
196 not_started,
197 in_progress,
198 ported,
199 verified,
200 skipped,
201 percent_complete,
202 }
203 }
204
205 pub fn remaining(&self) -> Vec<&TranslationMapping> {
208 self.mappings
209 .iter()
210 .filter(|m| !m.status.is_complete())
211 .collect()
212 }
213
214 pub fn completed(&self) -> Vec<&TranslationMapping> {
217 self.mappings
218 .iter()
219 .filter(|m| m.status.is_complete())
220 .collect()
221 }
222}
223
224#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn record_and_status() {
234 let mut tm = TranslationMap::new("ctx-1".into(), "ctx-2".into());
235 tm.record("foo", Some("foo_rs"), TranslationStatus::Ported, None);
236
237 let m = tm.status("foo").unwrap();
238 assert_eq!(m.target_symbol.as_deref(), Some("foo_rs"));
239 assert_eq!(m.status, TranslationStatus::Ported);
240 }
241
242 #[test]
243 fn record_updates_existing() {
244 let mut tm = TranslationMap::new("ctx-1".into(), "ctx-2".into());
245 tm.record("bar", None, TranslationStatus::NotStarted, None);
246 tm.record(
247 "bar",
248 Some("bar_rs"),
249 TranslationStatus::InProgress,
250 Some("WIP".into()),
251 );
252
253 assert_eq!(tm.mappings.len(), 1, "should update, not duplicate");
254 let m = tm.status("bar").unwrap();
255 assert_eq!(m.status, TranslationStatus::InProgress);
256 assert_eq!(m.notes.as_deref(), Some("WIP"));
257 }
258
259 #[test]
260 fn progress_calculation() {
261 let mut tm = TranslationMap::new("a".into(), "b".into());
262 tm.record("s1", None, TranslationStatus::NotStarted, None);
263 tm.record("s2", None, TranslationStatus::InProgress, None);
264 tm.record("s3", Some("t3"), TranslationStatus::Ported, None);
265 tm.record("s4", Some("t4"), TranslationStatus::Verified, None);
266 tm.record("s5", None, TranslationStatus::Skipped, None);
267
268 let p = tm.progress();
269 assert_eq!(p.total, 5);
270 assert_eq!(p.not_started, 1);
271 assert_eq!(p.in_progress, 1);
272 assert_eq!(p.ported, 1);
273 assert_eq!(p.verified, 1);
274 assert_eq!(p.skipped, 1);
275 assert!((p.percent_complete - 60.0).abs() < 0.01);
277 }
278
279 #[test]
280 fn progress_empty() {
281 let tm = TranslationMap::new("a".into(), "b".into());
282 let p = tm.progress();
283 assert_eq!(p.total, 0);
284 assert!((p.percent_complete - 0.0).abs() < 0.01);
285 }
286
287 #[test]
288 fn remaining_and_completed() {
289 let mut tm = TranslationMap::new("a".into(), "b".into());
290 tm.record("s1", None, TranslationStatus::NotStarted, None);
291 tm.record("s2", None, TranslationStatus::InProgress, None);
292 tm.record("s3", Some("t3"), TranslationStatus::Ported, None);
293 tm.record("s4", Some("t4"), TranslationStatus::Verified, None);
294 tm.record("s5", None, TranslationStatus::Skipped, None);
295
296 let rem: Vec<_> = tm.remaining().iter().map(|m| m.source_symbol.as_str()).collect();
297 assert_eq!(rem, vec!["s1", "s2"]);
298
299 let done: Vec<_> = tm.completed().iter().map(|m| m.source_symbol.as_str()).collect();
300 assert_eq!(done, vec!["s3", "s4", "s5"]);
301 }
302
303 #[test]
304 fn translation_status_roundtrip() {
305 for label in &["not_started", "in_progress", "ported", "verified", "skipped"] {
306 let status = TranslationStatus::from_str(label).unwrap();
307 assert_eq!(status.label(), *label);
308 }
309 assert_eq!(
311 TranslationStatus::from_str("not-started"),
312 Some(TranslationStatus::NotStarted)
313 );
314 assert_eq!(
315 TranslationStatus::from_str("in-progress"),
316 Some(TranslationStatus::InProgress)
317 );
318 assert!(TranslationStatus::from_str("bogus").is_none());
319 }
320
321 #[test]
322 fn status_returns_none_for_unknown() {
323 let tm = TranslationMap::new("a".into(), "b".into());
324 assert!(tm.status("nonexistent").is_none());
325 }
326}