Skip to main content

ralph/queue/loader/
read.rs

1//! Queue loader entrypoints for plain reads and parse repair.
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//! - Keep semantic repair writes out of read paths.
7//!
8//! Not handled here:
9//! - Timestamp normalization details (see `queue::repair`).
10//! - Validation rule definitions (see `queue::validation`).
11//!
12//! Invariants/assumptions:
13//! - Read-only flows never write queue or done files.
14//! - Semantic repair application is handled by undo-backed queue repair APIs.
15
16use super::validation::validate_loaded_queues;
17use crate::config::Resolved;
18use crate::contracts::QueueFile;
19use crate::queue::json_repair::attempt_json_repair;
20use crate::queue::validation::{self, ValidationWarning};
21use anyhow::{Context, Result};
22use std::path::Path;
23
24/// Load queue from path, returning default if file doesn't exist.
25pub fn load_queue_or_default(path: &Path) -> Result<QueueFile> {
26    if !path.exists() {
27        return Ok(QueueFile::default());
28    }
29    load_queue(path)
30}
31
32/// Load queue from path with standard JSONC parsing.
33pub fn load_queue(path: &Path) -> Result<QueueFile> {
34    let raw = std::fs::read_to_string(path)
35        .with_context(|| format!("read queue file {}", path.display()))?;
36    let queue = crate::jsonc::parse_jsonc::<QueueFile>(&raw, &format!("queue {}", path.display()))?;
37    Ok(queue)
38}
39
40/// Load queue with automatic repair for common JSON errors.
41/// Attempts to fix trailing commas and other common agent-induced mistakes.
42pub fn load_queue_with_repair(path: &Path) -> Result<QueueFile> {
43    let raw = std::fs::read_to_string(path)
44        .with_context(|| format!("read queue file {}", path.display()))?;
45
46    match crate::jsonc::parse_jsonc::<QueueFile>(&raw, &format!("queue {}", path.display())) {
47        Ok(queue) => Ok(queue),
48        Err(parse_err) => {
49            log::warn!("Queue JSON parse error, attempting repair: {}", parse_err);
50
51            if let Some(repaired) = attempt_json_repair(&raw) {
52                match crate::jsonc::parse_jsonc::<QueueFile>(
53                    &repaired,
54                    &format!("repaired queue {}", path.display()),
55                ) {
56                    Ok(queue) => {
57                        log::info!("Successfully repaired queue JSON");
58                        Ok(queue)
59                    }
60                    Err(repair_err) => Err(parse_err).with_context(|| {
61                        format!(
62                            "parse queue {} as JSON/JSONC (repair also failed: {})",
63                            path.display(),
64                            repair_err
65                        )
66                    })?,
67                }
68            } else {
69                Err(parse_err)
70            }
71        }
72    }
73}
74
75/// Load queue with JSON repair and semantic validation.
76///
77/// This API is pure with respect to the filesystem: it may repair parseable JSON
78/// mistakes in memory, but it never rewrites the queue file on disk.
79///
80/// Returns the queue file and any validation warnings (non-blocking issues).
81pub fn load_queue_with_repair_and_validate(
82    path: &Path,
83    done: Option<&crate::contracts::QueueFile>,
84    id_prefix: &str,
85    id_width: usize,
86    max_dependency_depth: u8,
87) -> Result<(QueueFile, Vec<ValidationWarning>)> {
88    let queue = load_queue_with_repair(path)?;
89
90    let warnings = if let Some(d) = done {
91        validation::validate_queue_set(&queue, Some(d), id_prefix, id_width, max_dependency_depth)
92            .with_context(|| format!("validate repaired queue {}", path.display()))?
93    } else {
94        validation::validate_queue(&queue, id_prefix, id_width)
95            .with_context(|| format!("validate repaired queue {}", path.display()))?;
96        Vec::new()
97    };
98
99    Ok((queue, warnings))
100}
101
102fn load_queue_set_with_repair(
103    resolved: &Resolved,
104    include_done: bool,
105) -> Result<(QueueFile, QueueFile, bool)> {
106    let queue_file = load_queue_with_repair(&resolved.queue_path)?;
107    let done_path_exists = resolved.done_path.exists();
108    let done_file = if done_path_exists {
109        load_queue_with_repair(&resolved.done_path)?
110    } else {
111        QueueFile::default()
112    };
113
114    let done_file = if include_done || done_path_exists {
115        done_file
116    } else {
117        QueueFile::default()
118    };
119
120    Ok((queue_file, done_file, done_path_exists))
121}
122
123/// Load the active queue and optionally the done queue, validating both.
124///
125/// This API is pure with respect to the filesystem: it may repair parseable JSON
126/// in memory, but it never rewrites queue/done files during the read.
127pub fn load_and_validate_queues(
128    resolved: &Resolved,
129    include_done: bool,
130) -> Result<(QueueFile, Option<QueueFile>)> {
131    let (queue_file, done_for_validation, _done_path_exists) =
132        load_queue_set_with_repair(resolved, include_done)?;
133    validate_loaded_queues(resolved, &queue_file, &done_for_validation)?;
134
135    let done_file = if include_done {
136        Some(done_for_validation)
137    } else {
138        None
139    };
140
141    Ok((queue_file, done_file))
142}