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