Skip to content

Commit a00cd1f

Browse files
committed
feat: add DAG catalog and ready planning
Expose nested DAG links and execution hints so an external orchestrator can traverse the review/eval graphs and ask for the next runnable frontier instead of treating the contracts as static metadata. Made-with: Cursor
1 parent e6c6cf6 commit a00cd1f

File tree

7 files changed

+440
-16
lines changed

7 files changed

+440
-16
lines changed

src/commands/dag.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use anyhow::Result;
2+
3+
use crate::config;
4+
use crate::core::dag::{plan_dag_execution, DagCatalog, DagExecutionPlan, DagGraphContract};
5+
use crate::review::{describe_review_pipeline_graph, describe_review_postprocess_graph};
6+
7+
use super::eval::describe_eval_fixture_graph;
8+
9+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10+
pub(crate) enum DagGraphSelection {
11+
Review,
12+
Postprocess { convention_store_path: bool },
13+
Eval { repro_validate: bool },
14+
}
15+
16+
pub(crate) fn describe_dag_graph(
17+
config: &config::Config,
18+
selection: DagGraphSelection,
19+
) -> DagGraphContract {
20+
match selection {
21+
DagGraphSelection::Review => describe_review_pipeline_graph(),
22+
DagGraphSelection::Postprocess {
23+
convention_store_path,
24+
} => describe_review_postprocess_graph(config, convention_store_path),
25+
DagGraphSelection::Eval { repro_validate } => describe_eval_fixture_graph(repro_validate),
26+
}
27+
}
28+
29+
pub(crate) fn build_dag_catalog(
30+
config: &config::Config,
31+
repro_validate: bool,
32+
convention_store_path: bool,
33+
) -> DagCatalog {
34+
DagCatalog {
35+
graphs: vec![
36+
describe_dag_graph(config, DagGraphSelection::Review),
37+
describe_dag_graph(
38+
config,
39+
DagGraphSelection::Postprocess {
40+
convention_store_path,
41+
},
42+
),
43+
describe_dag_graph(config, DagGraphSelection::Eval { repro_validate }),
44+
],
45+
}
46+
}
47+
48+
pub(crate) fn plan_dag_graph(
49+
config: &config::Config,
50+
selection: DagGraphSelection,
51+
completed: &[String],
52+
) -> Result<DagExecutionPlan> {
53+
let graph = describe_dag_graph(config, selection);
54+
plan_dag_execution(&graph, completed)
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use super::*;
60+
61+
#[test]
62+
fn dag_catalog_includes_nested_eval_and_review_graphs() {
63+
let catalog = build_dag_catalog(&config::Config::default(), true, true);
64+
65+
assert_eq!(catalog.graphs.len(), 3);
66+
assert_eq!(catalog.graphs[0].name, "review_pipeline");
67+
assert_eq!(catalog.graphs[1].name, "review_postprocess");
68+
assert_eq!(catalog.graphs[2].name, "eval_fixture_execution");
69+
}
70+
71+
#[test]
72+
fn dag_planner_reports_ready_nodes_for_review_pipeline() {
73+
let plan = plan_dag_graph(
74+
&config::Config::default(),
75+
DagGraphSelection::Review,
76+
&["initialize_services".to_string()],
77+
)
78+
.unwrap();
79+
80+
assert_eq!(plan.ready, vec!["build_session"]);
81+
}
82+
}

src/commands/eval/runner/execute/dag.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use tracing::debug;
55
use crate::config;
66
use crate::core;
77
use crate::core::dag::{
8-
describe_dag, execute_dag, DagGraphContract, DagNode, DagNodeContract, DagNodeKind, DagNodeSpec,
8+
describe_dag, execute_dag, DagGraphContract, DagNode, DagNodeContract, DagNodeExecutionHints,
9+
DagNodeKind, DagNodeSpec,
910
};
1011
use crate::core::eval_benchmarks::FixtureResult as BenchmarkFixtureResult;
1112
use crate::review::review_diff_content_raw;
@@ -153,6 +154,12 @@ pub(in super::super::super) fn describe_eval_fixture_graph(
153154
"verification_report".to_string(),
154155
"agent_activity".to_string(),
155156
],
157+
hints: DagNodeExecutionHints {
158+
parallelizable: false,
159+
retryable: true,
160+
side_effects: false,
161+
subgraph: Some("review_pipeline".to_string()),
162+
},
156163
enabled: spec.enabled,
157164
},
158165
EvalFixtureStage::ExpectationMatching => DagNodeContract {
@@ -168,6 +175,12 @@ pub(in super::super::super) fn describe_eval_fixture_graph(
168175
.collect(),
169176
inputs: vec!["comments".to_string(), "fixture_expectations".to_string()],
170177
outputs: vec!["match_summary".to_string(), "failures".to_string()],
178+
hints: DagNodeExecutionHints {
179+
parallelizable: true,
180+
retryable: true,
181+
side_effects: false,
182+
subgraph: None,
183+
},
171184
enabled: spec.enabled,
172185
},
173186
EvalFixtureStage::CommentCountValidation => DagNodeContract {
@@ -186,6 +199,12 @@ pub(in super::super::super) fn describe_eval_fixture_graph(
186199
"failures".to_string(),
187200
],
188201
outputs: vec!["failures".to_string()],
202+
hints: DagNodeExecutionHints {
203+
parallelizable: true,
204+
retryable: true,
205+
side_effects: false,
206+
subgraph: None,
207+
},
189208
enabled: spec.enabled,
190209
},
191210
EvalFixtureStage::BenchmarkMetrics => DagNodeContract {
@@ -205,6 +224,12 @@ pub(in super::super::super) fn describe_eval_fixture_graph(
205224
"failures".to_string(),
206225
],
207226
outputs: vec!["benchmark_metrics".to_string()],
227+
hints: DagNodeExecutionHints {
228+
parallelizable: true,
229+
retryable: true,
230+
side_effects: false,
231+
subgraph: None,
232+
},
208233
enabled: spec.enabled,
209234
},
210235
EvalFixtureStage::ReproductionValidation => DagNodeContract {
@@ -224,6 +249,12 @@ pub(in super::super::super) fn describe_eval_fixture_graph(
224249
"comments".to_string(),
225250
],
226251
outputs: vec!["reproduction_summary".to_string(), "warnings".to_string()],
252+
hints: DagNodeExecutionHints {
253+
parallelizable: false,
254+
retryable: true,
255+
side_effects: false,
256+
subgraph: None,
257+
},
227258
enabled: spec.enabled,
228259
},
229260
EvalFixtureStage::ArtifactCapture => DagNodeContract {
@@ -245,6 +276,12 @@ pub(in super::super::super) fn describe_eval_fixture_graph(
245276
"benchmark_metrics".to_string(),
246277
],
247278
outputs: vec!["artifact_path".to_string()],
279+
hints: DagNodeExecutionHints {
280+
parallelizable: false,
281+
retryable: true,
282+
side_effects: true,
283+
subgraph: None,
284+
},
248285
enabled: spec.enabled,
249286
},
250287
})
@@ -462,6 +499,9 @@ mod tests {
462499

463500
assert_eq!(graph.name, "eval_fixture_execution");
464501
assert_eq!(graph.entry_nodes, vec!["review"]);
502+
assert!(graph.nodes.iter().any(|node| {
503+
node.name == "review" && node.hints.subgraph.as_deref() == Some("review_pipeline")
504+
}));
465505
assert!(graph.nodes.iter().any(|node| {
466506
node.name == "reproduction_validation"
467507
&& node.outputs.contains(&"reproduction_summary".to_string())

src/commands/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod dag;
12
mod doctor;
23
mod eval;
34
mod feedback_eval;
@@ -7,8 +8,8 @@ mod pr;
78
mod review;
89
mod smart_review;
910

11+
pub(crate) use dag::{build_dag_catalog, describe_dag_graph, plan_dag_graph, DagGraphSelection};
1012
pub use doctor::doctor_command;
11-
pub(crate) use eval::describe_eval_fixture_graph;
1213
pub use eval::{eval_command, EvalRunOptions};
1314
pub use feedback_eval::feedback_eval_command;
1415
pub use git::{git_command, GitCommands};

src/core/dag.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub struct DagNodeContract {
4343
pub dependencies: Vec<String>,
4444
pub inputs: Vec<String>,
4545
pub outputs: Vec<String>,
46+
pub hints: DagNodeExecutionHints,
4647
pub enabled: bool,
4748
}
4849

@@ -55,6 +56,37 @@ pub struct DagGraphContract {
5556
pub nodes: Vec<DagNodeContract>,
5657
}
5758

59+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60+
pub struct DagNodeExecutionHints {
61+
pub parallelizable: bool,
62+
pub retryable: bool,
63+
pub side_effects: bool,
64+
pub subgraph: Option<String>,
65+
}
66+
67+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68+
pub struct DagCatalog {
69+
pub graphs: Vec<DagGraphContract>,
70+
}
71+
72+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73+
pub struct DagNodePlanState {
74+
pub name: String,
75+
pub enabled: bool,
76+
pub completed: bool,
77+
pub ready: bool,
78+
pub unmet_dependencies: Vec<String>,
79+
pub subgraph: Option<String>,
80+
}
81+
82+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83+
pub struct DagExecutionPlan {
84+
pub graph_name: String,
85+
pub completed: Vec<String>,
86+
pub ready: Vec<String>,
87+
pub nodes: Vec<DagNodePlanState>,
88+
}
89+
5890
#[derive(Debug, Clone, PartialEq, Eq)]
5991
pub struct DagExecutionRecord {
6092
pub name: String,
@@ -123,6 +155,50 @@ where
123155
Ok(records)
124156
}
125157

158+
pub fn plan_dag_execution(
159+
graph: &DagGraphContract,
160+
completed: &[String],
161+
) -> Result<DagExecutionPlan> {
162+
let completed_set = completed.iter().cloned().collect::<HashSet<_>>();
163+
164+
for name in &completed_set {
165+
if !graph.nodes.iter().any(|node| node.name == *name) {
166+
anyhow::bail!("Unknown completed node '{}' for DAG '{}'", name, graph.name);
167+
}
168+
}
169+
170+
let mut ready = Vec::new();
171+
let mut nodes = Vec::with_capacity(graph.nodes.len());
172+
for node in &graph.nodes {
173+
let unmet_dependencies = node
174+
.dependencies
175+
.iter()
176+
.filter(|dependency| !completed_set.contains(*dependency))
177+
.cloned()
178+
.collect::<Vec<_>>();
179+
let is_completed = completed_set.contains(&node.name);
180+
let is_ready = node.enabled && !is_completed && unmet_dependencies.is_empty();
181+
if is_ready {
182+
ready.push(node.name.clone());
183+
}
184+
nodes.push(DagNodePlanState {
185+
name: node.name.clone(),
186+
enabled: node.enabled,
187+
completed: is_completed,
188+
ready: is_ready,
189+
unmet_dependencies,
190+
subgraph: node.hints.subgraph.clone(),
191+
});
192+
}
193+
194+
Ok(DagExecutionPlan {
195+
graph_name: graph.name.clone(),
196+
completed: completed.to_vec(),
197+
ready,
198+
nodes,
199+
})
200+
}
201+
126202
#[cfg(test)]
127203
mod tests {
128204
use super::*;
@@ -199,4 +275,52 @@ mod tests {
199275
assert_eq!(descriptions[1].dependencies, vec!["root"]);
200276
assert!(!descriptions[1].enabled);
201277
}
278+
279+
#[test]
280+
fn plan_dag_execution_marks_ready_nodes() {
281+
let graph = DagGraphContract {
282+
name: "test".to_string(),
283+
description: "test graph".to_string(),
284+
entry_nodes: vec!["root".to_string()],
285+
terminal_nodes: vec!["leaf".to_string()],
286+
nodes: vec![
287+
DagNodeContract {
288+
name: "root".to_string(),
289+
description: "root".to_string(),
290+
kind: DagNodeKind::Setup,
291+
dependencies: vec![],
292+
inputs: vec![],
293+
outputs: vec![],
294+
hints: DagNodeExecutionHints {
295+
parallelizable: false,
296+
retryable: true,
297+
side_effects: false,
298+
subgraph: None,
299+
},
300+
enabled: true,
301+
},
302+
DagNodeContract {
303+
name: "leaf".to_string(),
304+
description: "leaf".to_string(),
305+
kind: DagNodeKind::Execution,
306+
dependencies: vec!["root".to_string()],
307+
inputs: vec![],
308+
outputs: vec![],
309+
hints: DagNodeExecutionHints {
310+
parallelizable: true,
311+
retryable: true,
312+
side_effects: false,
313+
subgraph: Some("child".to_string()),
314+
},
315+
enabled: true,
316+
},
317+
],
318+
};
319+
320+
let plan = plan_dag_execution(&graph, &["root".to_string()]).unwrap();
321+
322+
assert_eq!(plan.ready, vec!["leaf"]);
323+
assert!(plan.nodes[0].completed);
324+
assert_eq!(plan.nodes[1].subgraph.as_deref(), Some("child"));
325+
}
202326
}

0 commit comments

Comments
 (0)