Skip to main content

ralph/queue/loader/
read.rs

1//! Queue loader entrypoints for plain reads, parse repair, and explicit repair flows.
2//!
3//! Responsibilities:
4//! - Load queue files with plain JSONC parsing or in-memory parse repair.
5//! - Coordinate queue/done loading for read-only validation flows.
6//! - Expose explicit repair-and-validate flows that persist queue maintenance.
7//!
8//! Not handled here:
9//! - Timestamp normalization details (see `maintenance`).
10//! - Validation rule definitions (see `queue::validation`).
11//!
12//! Invariants/assumptions:
13//! - Read-only flows never write queue or done files.
14//! - Explicit repair flows may persist normalized timestamps and backfilled fields.
15
16use super::maintenance::maintain_and_save_loaded_queues;
17use super::validation::validate_loaded_queues;
18use crate::config::Resolved;
19use crate::contracts::QueueFile;
20use crate::queue::json_repair::attempt_json_repair;
21use crate::queue::validation::{self, ValidationWarning};
22use anyhow::{Context, Result};
23use std::path::Path;
24
25/// Load queue from path, returning default if file doesn't exist.
26pub fn load_queue_or_default(path: &Path) -> Result<QueueFile> {
27    if !path.exists() {
28        return Ok(QueueFile::default());
29    }
30    load_queue(path)
31}
32
33/// Load queue from path with standard JSONC parsing.
34pub fn load_queue(path: &Path) -> Result<QueueFile> {
35    let raw = std::fs::read_to_string(path)
36        .with_context(|| format!("read queue file {}", path.display()))?;
37    let queue = crate::jsonc::parse_jsonc::<QueueFile>(&raw, &format!("queue {}", path.display()))?;
38    Ok(queue)
39}
40
41/// Load queue with automatic repair for common JSON errors.
42/// Attempts to fix trailing commas and other common agent-induced mistakes.
43pub fn load_queue_with_repair(path: &Path) -> Result<QueueFile> {
44    let raw = std::fs::read_to_string(path)
45        .with_context(|| format!("read queue file {}", path.display()))?;
46
47    match crate::jsonc::parse_jsonc::<QueueFile>(&raw, &format!("queue {}", path.display())) {
48        Ok(queue) => Ok(queue),
49        Err(parse_err) => {
50            log::warn!("Queue JSON parse error, attempting repair: {}", parse_err);
51
52            if let Some(repaired) = attempt_json_repair(&raw) {
53                match crate::jsonc::parse_jsonc::<QueueFile>(
54                    &repaired,
55                    &format!("repaired queue {}", path.display()),
56                ) {
57                    Ok(queue) => {
58                        log::info!("Successfully repaired queue JSON");
59                        Ok(queue)
60                    }
61                    Err(repair_err) => Err(parse_err).with_context(|| {
62                        format!(
63                            "parse queue {} as JSON/JSONC (repair also failed: {})",
64                            path.display(),
65                            repair_err
66                        )
67                    })?,
68                }
69            } else {
70                Err(parse_err)
71            }
72        }
73    }
74}
75
76/// Load queue with JSON repair and semantic validation.
77///
78/// This API is pure with respect to the filesystem: it may repair parseable JSON
79/// mistakes in memory, but it never rewrites the queue file on disk.
80///
81/// Returns the queue file and any validation warnings (non-blocking issues).
82pub fn load_queue_with_repair_and_validate(
83    path: &Path,
84    done: Option<&crate::contracts::QueueFile>,
85    id_prefix: &str,
86    id_width: usize,
87    max_dependency_depth: u8,
88) -> Result<(QueueFile, Vec<ValidationWarning>)> {
89    let queue = load_queue_with_repair(path)?;
90
91    let warnings = if let Some(d) = done {
92        validation::validate_queue_set(&queue, Some(d), id_prefix, id_width, max_dependency_depth)
93            .with_context(|| format!("validate repaired queue {}", path.display()))?
94    } else {
95        validation::validate_queue(&queue, id_prefix, id_width)
96            .with_context(|| format!("validate repaired queue {}", path.display()))?;
97        Vec::new()
98    };
99
100    Ok((queue, warnings))
101}
102
103fn load_queue_set_with_repair(
104    resolved: &Resolved,
105    include_done: bool,
106) -> Result<(QueueFile, QueueFile, bool)> {
107    let queue_file = load_queue_with_repair(&resolved.queue_path)?;
108    let done_path_exists = resolved.done_path.exists();
109    let done_file = if done_path_exists {
110        load_queue_with_repair(&resolved.done_path)?
111    } else {
112        QueueFile::default()
113    };
114
115    let done_file = if include_done || done_path_exists {
116        done_file
117    } else {
118        QueueFile::default()
119    };
120
121    Ok((queue_file, done_file, done_path_exists))
122}
123
124/// Load the active queue and optionally the done queue, validating both.
125///
126/// This API is pure with respect to the filesystem: it may repair parseable JSON
127/// in memory, but it never rewrites queue/done files during the read.
128pub fn load_and_validate_queues(
129    resolved: &Resolved,
130    include_done: bool,
131) -> Result<(QueueFile, Option<QueueFile>)> {
132    let (queue_file, done_for_validation, _done_path_exists) =
133        load_queue_set_with_repair(resolved, include_done)?;
134    validate_loaded_queues(resolved, &queue_file, &done_for_validation)?;
135
136    let done_file = if include_done {
137        Some(done_for_validation)
138    } else {
139        None
140    };
141
142    Ok((queue_file, done_file))
143}
144
145/// Explicitly repair queue/done timestamp maintenance and persist the result before validation.
146///
147/// Unlike [`load_and_validate_queues`], this API mutates queue/done files on disk when it
148/// normalizes non-UTC timestamps or backfills missing terminal `completed_at` values.
149pub fn repair_and_validate_queues(
150    resolved: &Resolved,
151    include_done: bool,
152) -> Result<(QueueFile, Option<QueueFile>)> {
153    let (mut queue_file, mut done_for_validation, done_path_exists) =
154        load_queue_set_with_repair(resolved, true)?;
155
156    maintain_and_save_loaded_queues(
157        &resolved.queue_path,
158        &mut queue_file,
159        &resolved.done_path,
160        done_path_exists,
161        &mut done_for_validation,
162    )?;
163
164    validate_loaded_queues(resolved, &queue_file, &done_for_validation)?;
165
166    let done_file = if include_done {
167        Some(done_for_validation)
168    } else {
169        None
170    };
171
172    Ok((queue_file, done_file))
173}