Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/renderflow-core/data/profiles/everything-v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
schema: renderflow.profile/v1
description: Maximal policy-allowed artifact forest for the detected source
all_reachable: true
intermediates: cache_only
targets: []
include: {}
exclude: {}
8 changes: 8 additions & 0 deletions crates/renderflow-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ pub fn run_cli(cli: Cli) -> Result<()> {
resume,
optimization,
target,
profile,
exclude,
all,
}) => commands::build::run_selection(
&config,
dry_run,
resume,
optimization,
target.as_deref(),
profile.as_deref(),
&exclude,
all,
)?,
Some(Commands::Watch { config, debounce }) => commands::watch::run(&config, debounce)?,
Expand Down Expand Up @@ -105,12 +109,16 @@ pub fn run_cli(cli: Cli) -> Result<()> {
config,
format,
target,
profile,
exclude,
export,
optimization,
} => commands::graph::run_plan(
&config,
&format,
target.as_deref(),
profile.as_deref(),
&exclude,
export.as_deref(),
optimization,
)?,
Expand Down
27 changes: 22 additions & 5 deletions crates/renderflow-core/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub enum Commands {
renderflow build --optimization speed Build using speed optimization mode\n \
renderflow build --optimization pareto Build with Pareto-optimal path selection\n \
renderflow build --target pdf Build only the PDF output via graph resolution\n \
renderflow build --all Build all reachable outputs via graph resolution")]
renderflow build --profile everything Build the maximal available artifact forest")]
Build {
/// Path to the renderflow configuration file
#[arg(long, default_value = "renderflow.yaml", value_name = "FILE")]
Expand All @@ -84,13 +84,21 @@ pub enum Commands {
/// Build only the specified output format using the canonical capability graph.
/// Built-in document/image/audio capabilities and optional configured transforms are
/// resolved through the same planner. Cannot be combined with --all.
#[arg(long, value_name = "FORMAT", conflicts_with = "all")]
#[arg(long, value_name = "FORMAT", conflicts_with_all = ["all", "profile"])]
target: Option<String>,

/// Build a named, versioned derivative profile. `everything` is bundled.
#[arg(long, value_name = "PROFILE", conflicts_with_all = ["target", "all"])]
profile: Option<String>,

/// Exclude a branch selector (for example `family:video` or `provider:tool.ffmpeg`).
#[arg(long, value_name = "SELECTOR")]
exclude: Vec<String>,

/// Build all policy-allowed output formats reachable through the canonical capability graph.
/// Built-in capabilities and optional configured transforms participate equally.
/// Cannot be combined with --target.
#[arg(long, conflicts_with = "target")]
#[arg(long, conflicts_with_all = ["target", "profile"])]
all: bool,
},

Expand Down Expand Up @@ -437,7 +445,8 @@ pub enum GraphCommands {
renderflow graph plan\n \
renderflow graph plan --format mermaid\n \
renderflow graph plan --format json --export plan.json\n \
renderflow graph plan --target pdf")]
renderflow graph plan --target pdf\n \
renderflow graph plan --profile everything")]
Plan {
/// Path to the renderflow configuration file
#[arg(long, default_value = "renderflow.yaml", value_name = "FILE")]
Expand All @@ -449,9 +458,17 @@ pub enum GraphCommands {
format: String,

/// Limit the plan to this output format only.
#[arg(long, value_name = "FORMAT")]
#[arg(long, value_name = "FORMAT", conflicts_with = "profile")]
target: Option<String>,

/// Resolve a named, versioned derivative profile. `everything` is bundled.
#[arg(long, value_name = "PROFILE", conflicts_with = "target")]
profile: Option<String>,

/// Exclude a branch selector such as `family:video`.
#[arg(long, value_name = "SELECTOR")]
exclude: Vec<String>,

/// Write the output to a file instead of stdout.
#[arg(long, short = 'o', value_name = "FILE")]
export: Option<String>,
Expand Down
18 changes: 17 additions & 1 deletion crates/renderflow-core/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ use crate::planning::{execute, resolve, PlanningRequest};
/// Run the canonical Renderflow execution lifecycle using the target intent
/// declared in the v1/v2 configuration.
pub fn run(config_path: &str, dry_run: bool, optimization: Option<OptimizationMode>) -> Result<()> {
run_selection(config_path, dry_run, false, optimization, None, false)
run_selection(
config_path,
dry_run,
false,
optimization,
None,
None,
&[],
false,
)
}

/// Compatibility entrypoint for watch mode.
Expand All @@ -26,6 +35,8 @@ pub(crate) fn run_selection(
resume: bool,
optimization: Option<OptimizationMode>,
target: Option<&str>,
profile: Option<&str>,
exclude: &[String],
all_reachable: bool,
) -> Result<()> {
if dry_run {
Expand All @@ -40,9 +51,14 @@ pub(crate) fn run_selection(
}
if let Some(target) = target {
request = request.with_target(target);
} else if let Some(profile) = profile {
request = request.with_profile(profile);
} else if all_reachable {
request = request.with_all_reachable();
}
for selector in exclude {
request = request.with_exclude(selector)?;
}

let resolved = resolve(request)?.with_resume(resume);
info!(
Expand Down
31 changes: 28 additions & 3 deletions crates/renderflow-core/src/commands/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,27 @@ pub fn run_plan(
config_path: &str,
format: &str,
target: Option<&str>,
profile: Option<&str>,
exclude: &[String],
export: Option<&str>,
optimization: Option<OptimizationMode>,
) -> Result<()> {
let (plan, _) = load_plan(config_path, target, optimization)?;
let (plan, _) = if profile.is_some() || !exclude.is_empty() {
let mut request = PlanningRequest::from_path(config_path);
if let Some(profile) = profile {
request = request.with_profile(profile);
}
for selector in exclude {
request = request.with_exclude(selector)?;
}
if let Some(optimization) = optimization {
request = request.with_optimization(optimization);
}
let resolved = resolve(request)?;
(resolved.plan().clone(), resolved.target_formats())
} else {
load_plan(config_path, target, optimization)?
};

let renderer = renderer_for(format).ok_or_else(|| {
anyhow::anyhow!(
Expand All @@ -79,7 +96,7 @@ pub fn run_render(
export: Option<&str>,
optimization: Option<OptimizationMode>,
) -> Result<()> {
run_plan(config_path, format, target, export, optimization)
run_plan(config_path, format, target, None, &[], export, optimization)
}

// ── explain ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -126,7 +143,15 @@ pub fn run_export(
target: Option<&str>,
optimization: Option<OptimizationMode>,
) -> Result<()> {
run_plan(config_path, format, target, Some(output_path), optimization)
run_plan(
config_path,
format,
target,
None,
&[],
Some(output_path),
optimization,
)
}

// ── doctor ───────────────────────────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions crates/renderflow-core/src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ pub struct RunManifest {
pub diagnostics: Vec<ExecutionDiagnostic>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub toolchain: Option<ToolchainSnapshot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artifact_forest: Option<crate::graph::ArtifactForest>,
}

impl RunManifest {
Expand Down
54 changes: 54 additions & 0 deletions crates/renderflow-core/src/graph/execution_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,43 @@ pub struct PlanDiagnostic {
pub message: String,
}

/// Resolution state for one requested artifact-forest branch.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ForestBranchState {
Selected,
Excluded,
Unavailable,
BudgetPruned,
}

/// Machine-readable evidence for a branch considered during profile expansion.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForestBranch {
pub format: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
pub requirement: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub options: BTreeMap<String, serde_json::Value>,
pub state: ForestBranchState,
pub reason_code: String,
pub reason: String,
}

/// Requested and resolved artifact forest attached to plans and run manifests.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ArtifactForest {
#[serde(default)]
pub profiles: Vec<String>,
pub intermediates: String,
#[serde(default)]
pub branches: Vec<ForestBranch>,
/// Artifact ids actually produced; empty in a pre-execution plan.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub produced_artifacts: Vec<String>,
}

impl PlanDiagnostic {
fn info(message: impl Into<String>) -> Self {
PlanDiagnostic {
Expand Down Expand Up @@ -254,6 +291,9 @@ pub struct ExecutionPlan {
/// Reproducible evidence for providers selected by this exact plan.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub toolchain: Option<ToolchainSnapshot>,
/// Profile expansion and branch-local selection evidence.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artifact_forest: Option<ArtifactForest>,
}

impl ExecutionPlan {
Expand Down Expand Up @@ -366,6 +406,7 @@ impl ExecutionPlan {
diagnostics,
source_artifact: None,
toolchain: None,
artifact_forest: None,
}
}

Expand Down Expand Up @@ -394,6 +435,19 @@ impl ExecutionPlan {
self.source_artifact = Some(PlanSourceArtifact::from(report));
}

pub fn attach_artifact_forest(&mut self, forest: ArtifactForest) {
let selected = forest
.branches
.iter()
.filter(|branch| branch.state == ForestBranchState::Selected)
.count();
let pruned = forest.branches.len().saturating_sub(selected);
self.diagnostics.push(PlanDiagnostic::info(format!(
"Artifact forest selected {selected} branch(es) and recorded {pruned} excluded, unavailable, or budget-pruned branch(es)."
)));
self.artifact_forest = Some(forest);
}

/// Surface an unavailable/unsupported provider observation in plan diagnostics.
pub fn add_tool_diagnostic(&mut self, message: impl Into<String>) {
self.diagnostics.push(PlanDiagnostic::warning(message));
Expand Down
5 changes: 4 additions & 1 deletion crates/renderflow-core/src/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ mod transform_edge;
pub use dag_executor::{DagExecutionReport, DagExecutor};
pub use definition::TransformDefinition;
pub use definition_registry::TransformDefinitionRegistry;
pub use execution_plan::{DiagnosticLevel, ExecutionPlan, PlanSourceArtifact};
pub use execution_plan::{
ArtifactForest, DiagnosticLevel, ExecutionPlan, ForestBranch, ForestBranchState,
PlanSourceArtifact,
};
pub use format::Format;
pub use input_kind::InputKind;
pub use multi_target::MultiTargetDag;
Expand Down
Loading
Loading