Split validate.rs and resume.rs out of operations

Extract validate() into its own file from create.rs and resume() into
its own file from start.rs, maintaining one public operation per file.
Shared helpers (preprocess_and_validate, execute_persisted_run) become
pub(super) so the new modules can call them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-27 21:35:30 -04:00
parent b032ae32b4
commit 71e151c699
No known key found for this signature in database
5 changed files with 102 additions and 77 deletions

View file

@ -17,13 +17,6 @@ use super::source::{resolve_workflow, ResolveWorkflowInput, WorkflowInput};
const RUN_CONFIG_FILE: &str = "workflow.toml";
pub struct ValidateInput {
pub workflow: WorkflowInput,
pub settings: FabroSettings,
pub cwd: PathBuf,
pub custom_transforms: Vec<Box<dyn Transform>>,
}
#[derive(Clone, Debug)]
pub struct CreateRunInput {
pub workflow: WorkflowInput,
@ -57,27 +50,6 @@ struct PersistCreateOptions {
base_dir: Option<PathBuf>,
}
/// Parse, transform, and validate a DOT source string.
///
/// Returns `Validated` even when validation produced errors. Call
/// `validated.raise_on_errors()` if the caller wants to fail fast.
pub fn validate(input: ValidateInput) -> Result<Validated, FabroError> {
let resolved = resolve_workflow(ResolveWorkflowInput {
workflow: input.workflow,
settings: input.settings,
cwd: input.cwd,
})
.map_err(|err| FabroError::Parse(err.to_string()))?;
preprocess_and_validate(
&resolved.raw_source,
resolved.base_dir,
input.custom_transforms,
Some(&resolved.settings),
resolved.goal_override.as_deref(),
)
}
/// Resolve workflow inputs, normalize settings, and persist a run directory.
pub fn create(request: CreateRunInput) -> Result<CreatedRun, FabroError> {
let resolved = resolve_workflow(ResolveWorkflowInput {
@ -196,7 +168,7 @@ fn create_from_source(
persist_validated(validated, options)
}
fn preprocess_and_validate(
pub(super) fn preprocess_and_validate(
dot_source: &str,
base_dir: Option<PathBuf>,
custom_transforms: Vec<Box<dyn Transform>>,
@ -344,6 +316,7 @@ mod tests {
use super::*;
use fabro_graphviz::graph::AttrValue;
use crate::operations::{validate, ValidateInput};
use crate::run_status::RunStatusRecordExt;
fn validate_dot(dot_source: &str, settings: FabroSettings) -> Validated {

View file

@ -1,15 +1,19 @@
mod create;
mod fork;
mod resume;
mod rewind;
mod source;
mod start;
mod validate;
pub use crate::pipeline::{DevcontainerSpec, LlmSpec, SandboxEnvSpec, SandboxSpec};
pub use create::{create, validate, CreateRunInput, CreatedRun, ValidateInput};
pub use create::{create, CreateRunInput, CreatedRun};
pub use fork::{fork, ForkRunInput};
pub use resume::resume;
pub use rewind::{
build_timeline, find_run_id_by_prefix, rewind, RewindInput, RewindTarget, RunTimeline,
TimelineEntry,
};
pub use source::WorkflowInput;
pub use start::{resume, start, StartServices, Started};
pub use start::{start, StartServices, Started};
pub use validate::{validate, ValidateInput};

View file

@ -0,0 +1,52 @@
use std::path::Path;
use crate::error::FabroError;
use crate::outcome::StageStatus;
use crate::records::{Checkpoint, CheckpointExt, Conclusion, ConclusionExt};
use crate::run_status::{self, RunStatus, RunStatusRecordExt};
use super::start::{execute_persisted_run, StartServices, Started};
/// Resume a workflow run from its checkpoint. Errors if no checkpoint is found.
pub async fn resume(run_dir: &Path, services: StartServices) -> Result<Started, FabroError> {
if let Ok(record) = run_status::RunStatusRecord::load(&run_dir.join("status.json")) {
if record.status == RunStatus::Succeeded {
return Err(FabroError::Precondition(
"run already finished successfully — nothing to resume".to_string(),
));
}
}
if let Ok(conclusion) = Conclusion::load(&run_dir.join("conclusion.json")) {
if matches!(
conclusion.status,
StageStatus::Success | StageStatus::PartialSuccess | StageStatus::Skipped
) {
return Err(FabroError::Precondition(
"run already finished successfully — nothing to resume".to_string(),
));
}
}
let cp_path = run_dir.join("checkpoint.json");
let checkpoint = Checkpoint::load(&cp_path)
.map_err(|e| FabroError::Precondition(format!("no checkpoint to resume from: {e}")))?;
cleanup_resume_artifacts(run_dir);
run_status::write_run_status(run_dir, RunStatus::Submitted, None);
execute_persisted_run(run_dir, Some(checkpoint), services).await
}
fn cleanup_resume_artifacts(run_dir: &Path) {
for name in [
"conclusion.json",
"pull_request.json",
"detached_failure.json",
"interview_request.json",
"interview_response.json",
"interview_request.claim",
"progress.jsonl",
] {
let _ = std::fs::remove_file(run_dir.join(name));
}
}

View file

@ -22,7 +22,7 @@ use crate::pipeline::{
FinalizeOptions, Finalized, InitOptions, LlmSpec, Persisted, PullRequestOptions, RetroOptions,
SandboxEnvSpec, SandboxSpec,
};
use crate::records::{Checkpoint, CheckpointExt, Conclusion, ConclusionExt, RunRecordExt};
use crate::records::{Checkpoint, Conclusion, ConclusionExt, RunRecordExt};
use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions};
use crate::run_status::{self, RunStatus, RunStatusRecordExt, StatusReason};
@ -97,37 +97,7 @@ pub async fn start(run_dir: &Path, services: StartServices) -> Result<Started, F
execute_persisted_run(run_dir, None, services).await
}
/// Resume a workflow run from its checkpoint. Errors if no checkpoint is found.
pub async fn resume(run_dir: &Path, services: StartServices) -> Result<Started, FabroError> {
if let Ok(record) = run_status::RunStatusRecord::load(&run_dir.join("status.json")) {
if record.status == RunStatus::Succeeded {
return Err(FabroError::Precondition(
"run already finished successfully — nothing to resume".to_string(),
));
}
}
if let Ok(conclusion) = Conclusion::load(&run_dir.join("conclusion.json")) {
if matches!(
conclusion.status,
StageStatus::Success | StageStatus::PartialSuccess | StageStatus::Skipped
) {
return Err(FabroError::Precondition(
"run already finished successfully — nothing to resume".to_string(),
));
}
}
let cp_path = run_dir.join("checkpoint.json");
let checkpoint = Checkpoint::load(&cp_path)
.map_err(|e| FabroError::Precondition(format!("no checkpoint to resume from: {e}")))?;
cleanup_resume_artifacts(run_dir);
run_status::write_run_status(run_dir, RunStatus::Submitted, None);
execute_persisted_run(run_dir, Some(checkpoint), services).await
}
async fn execute_persisted_run(
pub(super) async fn execute_persisted_run(
run_dir: &Path,
checkpoint: Option<Checkpoint>,
services: StartServices,
@ -186,20 +156,6 @@ fn persist_terminal_engine_failure(run_dir: &Path, error: &FabroError, duration:
persist_terminal_outcome(run_dir, &conclusion, run_status, status_reason);
}
fn cleanup_resume_artifacts(run_dir: &Path) {
for name in [
"conclusion.json",
"pull_request.json",
"detached_failure.json",
"interview_request.json",
"interview_response.json",
"interview_request.claim",
"progress.jsonl",
] {
let _ = std::fs::remove_file(run_dir.join(name));
}
}
fn derive_start_options(
persisted: &Persisted,
services: StartServices,
@ -766,6 +722,8 @@ mod tests {
use crate::handler::exit::ExitHandler;
use crate::handler::start::StartHandler;
use crate::handler::HandlerRegistry;
use crate::operations::resume;
use crate::records::CheckpointExt;
const MINIMAL_DOT: &str = r#"digraph Test {
graph [goal="Build feature"]

View file

@ -0,0 +1,38 @@
use std::path::PathBuf;
use fabro_config::FabroSettings;
use crate::error::FabroError;
use crate::pipeline::Validated;
use crate::transforms::Transform;
use super::create::preprocess_and_validate;
use super::source::{resolve_workflow, ResolveWorkflowInput, WorkflowInput};
pub struct ValidateInput {
pub workflow: WorkflowInput,
pub settings: FabroSettings,
pub cwd: PathBuf,
pub custom_transforms: Vec<Box<dyn Transform>>,
}
/// Parse, transform, and validate a DOT source string.
///
/// Returns `Validated` even when validation produced errors. Call
/// `validated.raise_on_errors()` if the caller wants to fail fast.
pub fn validate(input: ValidateInput) -> Result<Validated, FabroError> {
let resolved = resolve_workflow(ResolveWorkflowInput {
workflow: input.workflow,
settings: input.settings,
cwd: input.cwd,
})
.map_err(|err| FabroError::Parse(err.to_string()))?;
preprocess_and_validate(
&resolved.raw_source,
resolved.base_dir,
input.custom_transforms,
Some(&resolved.settings),
resolved.goal_override.as_deref(),
)
}