mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
Return graphs from workflow transforms
This commit is contained in:
parent
48ea1cc4de
commit
6eb0c50d75
10 changed files with 87 additions and 62 deletions
|
|
@ -450,11 +450,14 @@ mod tests {
|
|||
struct TagTransform;
|
||||
|
||||
impl Transform for TagTransform {
|
||||
fn apply(&self, graph: &mut fabro_graphviz::graph::Graph) {
|
||||
fn apply(&self, graph: fabro_graphviz::graph::Graph) -> fabro_graphviz::graph::Graph {
|
||||
let mut graph = graph;
|
||||
for node in graph.nodes.values_mut() {
|
||||
node.attrs
|
||||
.insert("tagged".to_string(), AttrValue::Boolean(true));
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,30 +7,35 @@ use super::types::{Parsed, TransformOptions, Transformed};
|
|||
|
||||
/// TRANSFORM phase: apply built-in and custom transforms to a parsed graph.
|
||||
///
|
||||
/// Infallible. Returns `Transformed` with a mutable `graph` for post-transform
|
||||
/// Infallible. Returns `Transformed` with a graph for post-transform
|
||||
/// adjustments (e.g. goal override) before validation.
|
||||
pub fn transform(parsed: Parsed, options: &TransformOptions) -> Transformed {
|
||||
let Parsed { mut graph, source } = parsed;
|
||||
let Parsed { graph, source } = parsed;
|
||||
|
||||
// Built-in transforms (PreambleTransform moved to engine execution time)
|
||||
if let Some(ref dir) = options.base_dir {
|
||||
let graph = if let Some(ref dir) = options.base_dir {
|
||||
let fallback = dirs::home_dir().map(|home| home.join(".fabro"));
|
||||
ImportTransform::new(dir.clone(), fallback).apply(&mut graph);
|
||||
}
|
||||
ImportTransform::new(dir.clone(), fallback).apply(graph)
|
||||
} else {
|
||||
graph
|
||||
};
|
||||
|
||||
if let Some(ref dir) = options.base_dir {
|
||||
let graph = if let Some(ref dir) = options.base_dir {
|
||||
let fallback = dirs::home_dir().map(|home| home.join(".fabro"));
|
||||
FileInliningTransform::new(dir.clone(), fallback).apply(&mut graph);
|
||||
}
|
||||
FileInliningTransform::new(dir.clone(), fallback).apply(graph)
|
||||
} else {
|
||||
graph
|
||||
};
|
||||
|
||||
VariableExpansionTransform.apply(&mut graph);
|
||||
StylesheetApplicationTransform.apply(&mut graph);
|
||||
ModelResolutionTransform.apply(&mut graph);
|
||||
let graph = VariableExpansionTransform.apply(graph);
|
||||
let graph = StylesheetApplicationTransform.apply(graph);
|
||||
let graph = ModelResolutionTransform.apply(graph);
|
||||
|
||||
// Custom transforms
|
||||
for t in &options.custom_transforms {
|
||||
t.apply(&mut graph);
|
||||
}
|
||||
let graph = options
|
||||
.custom_transforms
|
||||
.iter()
|
||||
.fold(graph, |graph, transform| transform.apply(graph));
|
||||
|
||||
Transformed { graph, source }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ impl FileInliningTransform {
|
|||
}
|
||||
|
||||
impl Transform for FileInliningTransform {
|
||||
fn apply(&self, graph: &mut Graph) {
|
||||
fn apply(&self, graph: Graph) -> Graph {
|
||||
let mut graph = graph;
|
||||
let fallback = self.fallback_dir.as_deref();
|
||||
|
||||
// Inline @file refs in node prompts
|
||||
|
|
@ -95,6 +96,8 @@ impl Transform for FileInliningTransform {
|
|||
.insert("goal".to_string(), AttrValue::String(resolved));
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +176,7 @@ mod tests {
|
|||
graph.nodes.insert("work".to_string(), node);
|
||||
|
||||
let transform = FileInliningTransform::new(dir.path().to_path_buf(), None);
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["work"]
|
||||
|
|
@ -284,7 +287,7 @@ mod tests {
|
|||
base.path().to_path_buf(),
|
||||
Some(fallback.path().to_path_buf()),
|
||||
);
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["work"]
|
||||
|
|
|
|||
|
|
@ -168,8 +168,8 @@ impl ImportTransform {
|
|||
let import_base_dir = resolved_path
|
||||
.parent()
|
||||
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
|
||||
FileInliningTransform::new(import_base_dir.clone(), self.fallback_dir.clone())
|
||||
.apply(&mut graph);
|
||||
graph = FileInliningTransform::new(import_base_dir.clone(), self.fallback_dir.clone())
|
||||
.apply(graph);
|
||||
|
||||
if let Some(message) = Self::unresolved_imported_prompt_error(&graph) {
|
||||
return Err(message);
|
||||
|
|
@ -585,19 +585,22 @@ impl PreparedImport {
|
|||
}
|
||||
|
||||
impl Transform for ImportTransform {
|
||||
fn apply(&self, graph: &mut Graph) {
|
||||
let imports = Self::collect_import_nodes(graph);
|
||||
fn apply(&self, graph: Graph) -> Graph {
|
||||
let mut graph = graph;
|
||||
let imports = Self::collect_import_nodes(&graph);
|
||||
let mut import_stack = Vec::new();
|
||||
|
||||
for (placeholder_id, import_path) in imports {
|
||||
self.expand_import(
|
||||
graph,
|
||||
&mut graph,
|
||||
&placeholder_id,
|
||||
&import_path,
|
||||
&self.base_dir,
|
||||
&mut import_stack,
|
||||
);
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -622,10 +625,9 @@ mod tests {
|
|||
}
|
||||
|
||||
fn apply_import(dot: &str, base_dir: &Path, fallback_dir: Option<&Path>) -> Graph {
|
||||
let mut graph = parse_graph(dot);
|
||||
let graph = parse_graph(dot);
|
||||
ImportTransform::new(base_dir.to_path_buf(), fallback_dir.map(Path::to_path_buf))
|
||||
.apply(&mut graph);
|
||||
graph
|
||||
.apply(graph)
|
||||
}
|
||||
|
||||
fn basic_import_source() -> &'static str {
|
||||
|
|
@ -1327,7 +1329,7 @@ mod tests {
|
|||
graph
|
||||
.nodes
|
||||
.insert("validate.lint".to_string(), colliding_node);
|
||||
ImportTransform::new(dir.path().to_path_buf(), None).apply(&mut graph);
|
||||
let graph = ImportTransform::new(dir.path().to_path_buf(), None).apply(graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["validate"]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use fabro_graphviz::graph::Graph;
|
|||
|
||||
/// A transform that modifies the pipeline graph after parsing and before validation.
|
||||
pub trait Transform {
|
||||
fn apply(&self, graph: &mut Graph);
|
||||
#[must_use]
|
||||
fn apply(&self, graph: Graph) -> Graph;
|
||||
}
|
||||
|
||||
mod file_inlining;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use super::Transform;
|
|||
pub struct ModelResolutionTransform;
|
||||
|
||||
impl Transform for ModelResolutionTransform {
|
||||
fn apply(&self, graph: &mut Graph) {
|
||||
fn apply(&self, graph: Graph) -> Graph {
|
||||
let mut graph = graph;
|
||||
for node in graph.nodes.values_mut() {
|
||||
let model = node
|
||||
.attrs
|
||||
|
|
@ -29,6 +30,8 @@ impl Transform for ModelResolutionTransform {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +51,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ModelResolutionTransform.apply(&mut graph);
|
||||
let graph = ModelResolutionTransform.apply(graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"]
|
||||
|
|
@ -73,7 +76,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ModelResolutionTransform.apply(&mut graph);
|
||||
let graph = ModelResolutionTransform.apply(graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"]
|
||||
|
|
@ -94,7 +97,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ModelResolutionTransform.apply(&mut graph);
|
||||
let graph = ModelResolutionTransform.apply(graph);
|
||||
|
||||
assert_eq!(graph.nodes["a"].attrs.get("provider"), None);
|
||||
}
|
||||
|
|
@ -105,7 +108,7 @@ mod tests {
|
|||
let node = Node::new("a");
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ModelResolutionTransform.apply(&mut graph);
|
||||
let graph = ModelResolutionTransform.apply(graph);
|
||||
|
||||
assert_eq!(graph.nodes["a"].attrs.get("provider"), None);
|
||||
}
|
||||
|
|
@ -118,7 +121,7 @@ mod tests {
|
|||
.insert("model".to_string(), AttrValue::String("gpt-54".to_string()));
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ModelResolutionTransform.apply(&mut graph);
|
||||
let graph = ModelResolutionTransform.apply(graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"]
|
||||
|
|
@ -146,7 +149,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ModelResolutionTransform.apply(&mut graph);
|
||||
let graph = ModelResolutionTransform.apply(graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"]
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ use super::Transform;
|
|||
pub struct PreambleTransform;
|
||||
|
||||
impl Transform for PreambleTransform {
|
||||
fn apply(&self, graph: &mut Graph) {
|
||||
fn apply(&self, graph: Graph) -> Graph {
|
||||
use crate::context::keys::Fidelity;
|
||||
|
||||
let mut graph = graph;
|
||||
let default_fidelity = graph
|
||||
.default_fidelity()
|
||||
.and_then(|s| s.parse::<Fidelity>().ok())
|
||||
|
|
@ -28,6 +29,8 @@ impl Transform for PreambleTransform {
|
|||
.insert("prompt".to_string(), AttrValue::String(new_prompt));
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +54,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("work".to_string(), node);
|
||||
|
||||
PreambleTransform.apply(&mut graph);
|
||||
let graph = PreambleTransform.apply(graph);
|
||||
|
||||
let prompt = graph.nodes["work"]
|
||||
.attrs
|
||||
|
|
@ -75,7 +78,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("work".to_string(), node);
|
||||
|
||||
PreambleTransform.apply(&mut graph);
|
||||
let graph = PreambleTransform.apply(graph);
|
||||
|
||||
let prompt = graph.nodes["work"]
|
||||
.attrs
|
||||
|
|
@ -99,7 +102,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("work".to_string(), node);
|
||||
|
||||
PreambleTransform.apply(&mut graph);
|
||||
let graph = PreambleTransform.apply(graph);
|
||||
|
||||
let prompt = graph.nodes["work"]
|
||||
.attrs
|
||||
|
|
@ -119,7 +122,7 @@ mod tests {
|
|||
);
|
||||
graph.nodes.insert("work".to_string(), node);
|
||||
|
||||
PreambleTransform.apply(&mut graph);
|
||||
let graph = PreambleTransform.apply(graph);
|
||||
|
||||
assert!(!graph.nodes["work"].attrs.contains_key("prompt"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,17 @@ use super::stylesheet::{apply_stylesheet, parse_stylesheet};
|
|||
pub struct StylesheetApplicationTransform;
|
||||
|
||||
impl Transform for StylesheetApplicationTransform {
|
||||
fn apply(&self, graph: &mut Graph) {
|
||||
fn apply(&self, graph: Graph) -> Graph {
|
||||
let mut graph = graph;
|
||||
let stylesheet_text = graph.model_stylesheet().to_string();
|
||||
if stylesheet_text.is_empty() {
|
||||
return;
|
||||
return graph;
|
||||
}
|
||||
let Ok(stylesheet) = parse_stylesheet(&stylesheet_text) else {
|
||||
return;
|
||||
return graph;
|
||||
};
|
||||
apply_stylesheet(&stylesheet, graph);
|
||||
apply_stylesheet(&stylesheet, &mut graph);
|
||||
graph
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +34,6 @@ mod tests {
|
|||
|
||||
let transform = StylesheetApplicationTransform;
|
||||
// Should not panic with empty stylesheet
|
||||
transform.apply(&mut graph);
|
||||
let _graph = transform.apply(graph);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ pub fn expand_vars(source: &str, vars: &HashMap<String, String>) -> anyhow::Resu
|
|||
pub struct VariableExpansionTransform;
|
||||
|
||||
impl Transform for VariableExpansionTransform {
|
||||
fn apply(&self, graph: &mut Graph) {
|
||||
fn apply(&self, graph: Graph) -> Graph {
|
||||
let mut graph = graph;
|
||||
let goal = graph.goal().to_string();
|
||||
let vars = HashMap::from([("goal".to_string(), goal)]);
|
||||
for node in graph.nodes.values_mut() {
|
||||
|
|
@ -62,6 +63,8 @@ impl Transform for VariableExpansionTransform {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +129,7 @@ mod tests {
|
|||
graph.nodes.insert("plan".to_string(), node);
|
||||
|
||||
let transform = VariableExpansionTransform;
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
let prompt = graph.nodes["plan"]
|
||||
.attrs
|
||||
|
|
@ -152,7 +155,7 @@ mod tests {
|
|||
graph.nodes.insert("plan".to_string(), node);
|
||||
|
||||
let transform = VariableExpansionTransform;
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
let prompt = graph.nodes["plan"]
|
||||
.attrs
|
||||
|
|
@ -173,7 +176,7 @@ mod tests {
|
|||
graph.nodes.insert("plan".to_string(), node);
|
||||
|
||||
let transform = VariableExpansionTransform;
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
let prompt = graph.nodes["plan"]
|
||||
.attrs
|
||||
|
|
@ -195,7 +198,7 @@ mod tests {
|
|||
|
||||
let transform = VariableExpansionTransform;
|
||||
// Should not panic
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
assert!(!graph.nodes["plan"].attrs.contains_key("prompt"));
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +218,7 @@ mod tests {
|
|||
graph.nodes.insert("plan".to_string(), node);
|
||||
|
||||
let transform = VariableExpansionTransform;
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
let prompt = graph.nodes["plan"]
|
||||
.attrs
|
||||
|
|
|
|||
|
|
@ -963,7 +963,7 @@ fn variable_expansion_replaces_goal_in_prompts() {
|
|||
graph.nodes.insert("report".to_string(), no_var_node);
|
||||
|
||||
let transform = VariableExpansionTransform;
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
let plan_prompt = graph.nodes["plan"]
|
||||
.attrs
|
||||
|
|
@ -1028,7 +1028,7 @@ fn stylesheet_application_by_specificity() {
|
|||
graph.nodes.insert("explicit_node".to_string(), explicit);
|
||||
|
||||
let transform = StylesheetApplicationTransform;
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
// plan: universal -> claude-sonnet-4-5
|
||||
assert_eq!(
|
||||
|
|
@ -1084,11 +1084,11 @@ fn stylesheet_application_via_parsed_graph() {
|
|||
start -> work -> exit
|
||||
}"#;
|
||||
|
||||
let mut graph = parse(input).expect("parse should succeed");
|
||||
let graph = parse(input).expect("parse should succeed");
|
||||
validate_or_raise(&graph, &[]).expect("validation should pass");
|
||||
|
||||
let transform = StylesheetApplicationTransform;
|
||||
transform.apply(&mut graph);
|
||||
let graph = transform.apply(graph);
|
||||
|
||||
// All nodes without explicit model should get "sonnet"
|
||||
assert_eq!(
|
||||
|
|
@ -2482,9 +2482,9 @@ async fn scenario_ship_a_feature() {
|
|||
review -> exit [label="[A] Approve"]
|
||||
review -> implement [label="[F] Fix"]
|
||||
}"#;
|
||||
let mut graph = parse(dot).expect("parse");
|
||||
let graph = parse(dot).expect("parse");
|
||||
validate_or_raise(&graph, &[]).expect("validate");
|
||||
VariableExpansionTransform.apply(&mut graph);
|
||||
let graph = VariableExpansionTransform.apply(graph);
|
||||
assert_eq!(
|
||||
graph.nodes["plan"].prompt().unwrap(),
|
||||
"Plan to achieve: Ship the widget"
|
||||
|
|
@ -3418,9 +3418,9 @@ async fn stylesheet_applies_model_override() {
|
|||
work [shape=box, prompt="Do work"]
|
||||
start -> work -> exit
|
||||
}"#;
|
||||
let mut graph = parse(input).expect("parse");
|
||||
let graph = parse(input).expect("parse");
|
||||
validate_or_raise(&graph, &[]).expect("validate");
|
||||
StylesheetApplicationTransform.apply(&mut graph);
|
||||
let graph = StylesheetApplicationTransform.apply(graph);
|
||||
assert_eq!(graph.nodes["work"].model(), Some("custom-model"));
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
@ -3527,7 +3527,7 @@ async fn integration_smoke_plan_implement_review_done() {
|
|||
}"#;
|
||||
|
||||
// Parse and validate
|
||||
let mut graph = parse(dot).expect("parse");
|
||||
let graph = parse(dot).expect("parse");
|
||||
let diagnostics = validate_or_raise(&graph, &[]).expect("validate");
|
||||
let errors: Vec<_> = diagnostics
|
||||
.iter()
|
||||
|
|
@ -3536,8 +3536,8 @@ async fn integration_smoke_plan_implement_review_done() {
|
|||
assert!(errors.is_empty());
|
||||
|
||||
// Apply transforms
|
||||
VariableExpansionTransform.apply(&mut graph);
|
||||
StylesheetApplicationTransform.apply(&mut graph);
|
||||
let graph = VariableExpansionTransform.apply(graph);
|
||||
let graph = StylesheetApplicationTransform.apply(graph);
|
||||
|
||||
// Verify transforms applied
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue