Skip to main content

agentic_codebase/workspace/
translation.rs

1//! Translation tracking — monitors the porting status of symbols between
2//! a source context and a target context within a workspace.
3//!
4//! A [`TranslationMap`] records which source symbols have been ported, which
5//! are in progress, and which remain untouched. The [`TranslationProgress`]
6//! summary provides at-a-glance metrics for migration dashboards.
7
8// ---------------------------------------------------------------------------
9// TranslationStatus
10// ---------------------------------------------------------------------------
11
12/// The porting status of a single source symbol.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum TranslationStatus {
15    /// No work has begun on this symbol.
16    NotStarted,
17    /// Porting is underway but not yet complete.
18    InProgress,
19    /// The symbol has been ported to the target context.
20    Ported,
21    /// The ported symbol has been reviewed and verified.
22    Verified,
23    /// The symbol was intentionally excluded from porting.
24    Skipped,
25}
26
27impl TranslationStatus {
28    /// Parse a status from a string (case-insensitive).
29    ///
30    /// Accepts both hyphenated (`"not-started"`, `"in-progress"`) and
31    /// underscore (`"not_started"`, `"in_progress"`) variants.
32    pub fn parse_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    /// A human-readable label for this status.
44    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    /// Whether this status counts toward "complete" progress.
55    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// ---------------------------------------------------------------------------
67// TranslationMapping
68// ---------------------------------------------------------------------------
69
70/// A mapping from a single source symbol to its target counterpart.
71#[derive(Debug)]
72pub struct TranslationMapping {
73    /// Name of the symbol in the source context.
74    pub source_symbol: String,
75    /// Name of the corresponding symbol in the target context, if one exists.
76    pub target_symbol: Option<String>,
77    /// Current porting status.
78    pub status: TranslationStatus,
79    /// Free-form notes (e.g., "needs manual review", "API changed").
80    pub notes: Option<String>,
81}
82
83// ---------------------------------------------------------------------------
84// TranslationProgress
85// ---------------------------------------------------------------------------
86
87/// Summary statistics for a [`TranslationMap`].
88#[derive(Debug, Clone)]
89pub struct TranslationProgress {
90    /// Total number of tracked source symbols.
91    pub total: usize,
92    /// Symbols with [`TranslationStatus::NotStarted`].
93    pub not_started: usize,
94    /// Symbols with [`TranslationStatus::InProgress`].
95    pub in_progress: usize,
96    /// Symbols with [`TranslationStatus::Ported`].
97    pub ported: usize,
98    /// Symbols with [`TranslationStatus::Verified`].
99    pub verified: usize,
100    /// Symbols with [`TranslationStatus::Skipped`].
101    pub skipped: usize,
102    /// Percentage of symbols considered complete:
103    /// `(ported + verified + skipped) / total * 100.0`.
104    /// Returns `0.0` when there are no mappings.
105    pub percent_complete: f32,
106}
107
108// ---------------------------------------------------------------------------
109// TranslationMap
110// ---------------------------------------------------------------------------
111
112/// Tracks the porting status of symbols from one context to another.
113///
114/// Symbols are keyed by their source name. Calling [`record`](Self::record)
115/// with a source name that already exists will update the existing mapping
116/// rather than creating a duplicate.
117#[derive(Debug)]
118pub struct TranslationMap {
119    /// Context ID of the source codebase.
120    pub source_context: String,
121    /// Context ID of the target codebase.
122    pub target_context: String,
123    /// Ordered list of symbol mappings.
124    mappings: Vec<TranslationMapping>,
125}
126
127impl TranslationMap {
128    /// Create an empty translation map between two contexts.
129    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    /// Record or update a translation mapping.
138    ///
139    /// If a mapping for `source` already exists it is updated in place;
140    /// otherwise a new entry is appended.
141    pub fn record(
142        &mut self,
143        source: &str,
144        target: Option<&str>,
145        status: TranslationStatus,
146        notes: Option<String>,
147    ) {
148        // Update in place if the source symbol already exists.
149        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    /// Look up the current mapping for a source symbol.
165    pub fn status(&self, source: &str) -> Option<&TranslationMapping> {
166        self.mappings.iter().find(|m| m.source_symbol == source)
167    }
168
169    /// Compute aggregate progress across all tracked symbols.
170    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    /// Return all mappings that still need work ([`NotStarted`](TranslationStatus::NotStarted)
206    /// or [`InProgress`](TranslationStatus::InProgress)).
207    pub fn remaining(&self) -> Vec<&TranslationMapping> {
208        self.mappings
209            .iter()
210            .filter(|m| !m.status.is_complete())
211            .collect()
212    }
213
214    /// Return all mappings that are considered complete ([`Ported`](TranslationStatus::Ported),
215    /// [`Verified`](TranslationStatus::Verified), or [`Skipped`](TranslationStatus::Skipped)).
216    pub fn completed(&self) -> Vec<&TranslationMapping> {
217        self.mappings
218            .iter()
219            .filter(|m| m.status.is_complete())
220            .collect()
221    }
222}
223
224// ---------------------------------------------------------------------------
225// Tests
226// ---------------------------------------------------------------------------
227
228#[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        // (1 + 1 + 1) / 5 * 100 = 60.0
276        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
297            .remaining()
298            .iter()
299            .map(|m| m.source_symbol.as_str())
300            .collect();
301        assert_eq!(rem, vec!["s1", "s2"]);
302
303        let done: Vec<_> = tm
304            .completed()
305            .iter()
306            .map(|m| m.source_symbol.as_str())
307            .collect();
308        assert_eq!(done, vec!["s3", "s4", "s5"]);
309    }
310
311    #[test]
312    fn translation_status_roundtrip() {
313        for label in &[
314            "not_started",
315            "in_progress",
316            "ported",
317            "verified",
318            "skipped",
319        ] {
320            let status = TranslationStatus::parse_str(label).unwrap();
321            assert_eq!(status.label(), *label);
322        }
323        // Also accept hyphenated forms.
324        assert_eq!(
325            TranslationStatus::parse_str("not-started"),
326            Some(TranslationStatus::NotStarted)
327        );
328        assert_eq!(
329            TranslationStatus::parse_str("in-progress"),
330            Some(TranslationStatus::InProgress)
331        );
332        assert!(TranslationStatus::parse_str("bogus").is_none());
333    }
334
335    #[test]
336    fn status_returns_none_for_unknown() {
337        let tm = TranslationMap::new("a".into(), "b".into());
338        assert!(tm.status("nonexistent").is_none());
339    }
340}