Skip to main content

only_engine/
planner.rs

1use std::fmt;
2use std::path::PathBuf;
3
4use only_semantic::{DocumentAst, TaskAst};
5
6use crate::dag::expand_execution_order;
7use crate::resolve::{
8    build_execution_nodes, build_task_index, document_shell, merge_parameter_inputs,
9    resolve_root_task, resolve_root_task_in_document,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Invocation<'a> {
14    Task {
15        target: &'a str,
16        args: Vec<&'a str>,
17        overrides: Vec<(&'a str, &'a str)>,
18    },
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ExecutionNode {
23    pub stage: usize,
24    pub name: String,
25    pub commands: Vec<String>,
26    pub params: Vec<PlanParam>,
27    pub shell: Option<String>,
28    pub shell_fallback: bool,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct PlanParam {
33    pub name: String,
34    pub default_value: Option<String>,
35    pub value: Option<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
39pub struct ExecutionPlan {
40    pub nodes: Vec<ExecutionNode>,
41    pub shell: Option<String>,
42    pub working_dir: PathBuf,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum PlanError {
47    UnknownTask(String),
48    HelperTask(String),
49    TaskUnavailable(String),
50    MissingRequiredParameter(String),
51    UnknownParameter {
52        task: String,
53        name: String,
54    },
55    DuplicateOverride(String),
56    CyclicDependency(String),
57    TooManyArguments {
58        task: String,
59        expected: usize,
60        got: usize,
61    },
62}
63
64impl fmt::Display for PlanError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::UnknownTask(task) => write!(f, "task '{task}' is not defined"),
68            Self::HelperTask(task) => {
69                write!(f, "helper task '{task}' cannot be invoked directly")
70            }
71            Self::TaskUnavailable(task) => {
72                write!(f, "task '{task}' is not available for this environment")
73            }
74            Self::MissingRequiredParameter(name) => {
75                write!(f, "missing required parameter '{{{{{name}}}}}'")
76            }
77            Self::UnknownParameter { task, name } => {
78                write!(f, "unknown parameter '{name}' for task '{task}'")
79            }
80            Self::DuplicateOverride(name) => write!(f, "duplicate parameter override '{name}'"),
81            Self::CyclicDependency(path) => write!(f, "cyclic dependency detected: {path}"),
82            Self::TooManyArguments {
83                task,
84                expected,
85                got,
86            } => write!(
87                f,
88                "too many arguments for task '{task}'; expected at most {expected}, got {got}"
89            ),
90        }
91    }
92}
93
94impl std::error::Error for PlanError {}
95
96/// Builds a dependency-ordered execution plan from semantic AST.
97///
98/// Args:
99/// document: Semantic AST used by the runtime.
100/// invocation: Requested task target.
101///
102/// Returns:
103/// Dependency-expanded execution plan in DAG order.
104pub fn build_execution_plan(document: &DocumentAst, invocation: Invocation<'_>) -> ExecutionPlan {
105    let working_dir = std::env::current_dir().unwrap_or_default();
106    try_build_execution_plan_in_dir(document, invocation, working_dir).unwrap_or_default()
107}
108
109/// Builds a dependency-ordered execution plan from semantic AST and returns planner errors.
110///
111/// Args:
112/// document: Semantic AST used by the runtime.
113/// invocation: Requested task target plus input bindings.
114///
115/// Returns:
116/// Dependency-expanded execution plan in DAG order.
117pub fn try_build_execution_plan(
118    document: &DocumentAst,
119    invocation: Invocation<'_>,
120) -> Result<ExecutionPlan, PlanError> {
121    let working_dir = std::env::current_dir().unwrap_or_default();
122    try_build_execution_plan_in_dir(document, invocation, working_dir)
123}
124
125/// Builds a dependency-ordered execution plan from semantic AST for an explicit working directory.
126///
127/// Args:
128/// document: Semantic AST used by the runtime.
129/// invocation: Requested task target plus input bindings.
130/// working_dir: Directory used by runtime execution.
131///
132/// Returns:
133/// Dependency-expanded execution plan in DAG order.
134pub fn try_build_execution_plan_in_dir(
135    document: &DocumentAst,
136    invocation: Invocation<'_>,
137    working_dir: PathBuf,
138) -> Result<ExecutionPlan, PlanError> {
139    let Invocation::Task {
140        target,
141        args,
142        overrides,
143    } = invocation;
144
145    let tasks = build_task_index(document);
146    let root = resolve_root_task(&tasks, target)?;
147    let overrides = merge_parameter_inputs(args, overrides, root)?;
148    let ordered = expand_execution_order(root, &overrides, &tasks)?;
149
150    Ok(ExecutionPlan {
151        nodes: build_execution_nodes(ordered),
152        shell: document_shell(document),
153        working_dir,
154    })
155}
156
157/// Resolves the concrete root task variant selected for the current environment.
158///
159/// Args:
160/// document: Semantic AST used by the runtime.
161/// target: Fully qualified task target.
162///
163/// Returns:
164/// The selected root task variant or a planner error when unavailable.
165pub fn select_root_task_variant<'a>(
166    document: &'a DocumentAst,
167    target: &str,
168) -> Result<&'a TaskAst, PlanError> {
169    resolve_root_task_in_document(document, target)
170}