Move run loading into Run::open and rename OrphanedRun to OpenedRun
scan_orphans was duplicating the file-reading logic that belongs on
Run. Run::open now reads meta and state from disk, so scan_orphans
only does directory walking. OrphanedRun was just "a run loaded from
disk" so it is now OpenedRun.

Assisted-by: GLM-5.1 via pi
change upopnlxvotozszknlmkwzvuvzxtxprnr
commit 8ce6f9d55f8cffd5e0279fc2c463042e8951cc05
author Alpha Chen <alpha@kejadlen.dev>
date
parent xuzkmwpk
diff --git a/src/ci.rs b/src/ci.rs
index 709e36e..14ee1c9 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -102,7 +102,7 @@ impl Runs {
     /// The caller decides how to reconcile them:
     /// - `pending/` entries should be re-enqueued.
     /// - `active/` entries with no live runner should be marked failed.
-    pub fn scan_orphans(&self) -> Vec<OrphanedRun> {
+    pub fn scan_orphans(&self) -> Vec<OpenedRun> {
         let mut orphans = Vec::new();
 
         for &state in &[RunState::Pending, RunState::Active] {
@@ -122,37 +122,16 @@ impl Runs {
                     continue;
                 }
 
-                let path = entry.path();
-
-                let meta = match read_yaml::<RunMeta>(&path.join("meta.yml")) {
-                    Ok(m) => m,
-                    Err(e) => {
-                        tracing::warn!(
-                            path = %path.display(),
-                            %e,
-                            "skipping orphaned run: cannot read meta"
-                        );
-                        continue;
-                    }
-                };
-
-                let state_file = match read_yaml::<RunStateFile>(&path.join("state.yml")) {
-                    Ok(s) => s,
+                match Run::open(self.base.clone(), state, name) {
+                    Ok(opened) => orphans.push(opened),
                     Err(e) => {
                         tracing::warn!(
-                            path = %path.display(),
+                            state = ?state,
                             %e,
-                            "skipping orphaned run: cannot read state"
+                            "skipping orphaned run"
                         );
-                        continue;
                     }
-                };
-
-                orphans.push(OrphanedRun {
-                    run: Run::open(self.base.clone(), state, name),
-                    meta,
-                    state: state_file,
-                });
+                }
             }
         }
 
@@ -247,12 +226,20 @@ impl Run {
         self.state
     }
 
-    /// Open an existing run at a known state.
+    /// Open an existing run from disk, reading its metadata and state.
     ///
-    /// Does not verify the directory exists — used by the orphan scanner
-    /// which already read the directory listing.
-    fn open(base: PathBuf, state: RunState, id: String) -> Self {
-        Self { base, state, id }
+    /// `state` is the directory the run is expected to be in (e.g.
+    /// `pending/`, `active/`). Returns an error if the run directory or
+    /// its files are missing or unreadable.
+    pub fn open(base: PathBuf, state: RunState, id: String) -> Result<OpenedRun> {
+        let run = Self { base, state, id };
+        let meta = run.read_meta()?;
+        let run_state = run.read_state()?;
+        Ok(OpenedRun {
+            run,
+            meta,
+            state: run_state,
+        })
     }
 
     /// Transition the run from its current state to a new state.
@@ -293,9 +280,9 @@ impl Run {
     }
 }
 
-/// An orphaned run found during startup scan.
+/// A run loaded from disk with its metadata and state.
 #[derive(Debug)]
-pub struct OrphanedRun {
+pub struct OpenedRun {
     pub run: Run,
     pub meta: RunMeta,
     pub state: RunStateFile,