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