fabro(01KY7Y01REECZ24XXTMBZ3PPV9): simplify_fable (succeeded)

Fabro-Run: 01KY7Y01REECZ24XXTMBZ3PPV9
Fabro-Completed: 6
Fabro-Checkpoint: 497f92f4d9

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-07-23 19:10:39 +00:00
parent 1a2bd7966d
commit 56119990be
11 changed files with 262 additions and 170 deletions

1
Cargo.lock generated
View file

@ -2659,6 +2659,7 @@ dependencies = [
"nom",
"regex",
"serde",
"serde_json",
"strum 0.28.0",
"thiserror 2.0.18",
]

View file

@ -21,3 +21,6 @@ regex = { workspace = true }
serde = { workspace = true }
strum.workspace = true
thiserror = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true }

View file

@ -1,9 +1,24 @@
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, VariantArray};
/// Fidelity mode controlling how much prior context is provided to LLM
/// sessions.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display, EnumString, VariantArray)]
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
Hash,
Display,
EnumString,
VariantArray,
Serialize,
Deserialize,
)]
#[strum(serialize_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum Fidelity {
/// Complete context, no summarization — sessions share a thread.
Full,
@ -14,12 +29,15 @@ pub enum Fidelity {
Compact,
/// Brief textual summary (~600 token target).
#[strum(serialize = "summary:low")]
#[serde(rename = "summary:low")]
SummaryLow,
/// Moderate textual summary (~1500 token target).
#[strum(serialize = "summary:medium")]
#[serde(rename = "summary:medium")]
SummaryMedium,
/// Detailed per-stage Markdown report.
#[strum(serialize = "summary:high")]
#[serde(rename = "summary:high")]
SummaryHigh,
}
@ -73,4 +91,14 @@ mod tests {
fn fidelity_unknown_mode_errors() {
assert!("bogus".parse::<Fidelity>().is_err());
}
#[test]
fn fidelity_serde_matches_strum_display() {
for mode in Fidelity::variants() {
let json = serde_json::to_value(mode).unwrap();
assert_eq!(json, serde_json::Value::String(mode.to_string()));
let parsed: Fidelity = serde_json::from_value(json).unwrap();
assert_eq!(parsed, *mode);
}
}
}

View file

@ -35,22 +35,25 @@ impl<'a> ParallelBranches<'a> {
.collect()
}
/// True when every incoming edge of `node_id` comes from a parallel fork
/// (and there is at least one). Such a node only ever runs as a branch.
pub(super) fn is_branch_only_node(&self, node_id: &str) -> bool {
self.branch_only_parents(node_id).is_some()
let incoming = self.graph.incoming_edges(node_id);
!incoming.is_empty() && incoming.iter().all(|edge| self.is_fork_edge(edge))
}
/// The sorted, deduplicated fork parents of a branch-only node, or `None`
/// when the node has a non-fork entry path (or no entry at all).
pub(super) fn branch_only_parents(&self, node_id: &str) -> Option<Vec<String>> {
let mut incoming = self
if !self.is_branch_only_node(node_id) {
return None;
}
let parents: BTreeSet<&str> = self
.graph
.edges
.iter()
.filter(|edge| edge.to == node_id)
.peekable();
incoming.peek()?;
incoming
.map(|edge| self.is_fork_edge(edge).then(|| edge.from.clone()))
.collect::<Option<BTreeSet<_>>>()
.map(|parents| parents.into_iter().collect())
.incoming_edges(node_id)
.into_iter()
.map(|edge| edge.from.as_str())
.collect();
Some(parents.into_iter().map(String::from).collect())
}
}

View file

@ -7,11 +7,15 @@ pub(super) fn rule() -> Box<dyn LintRule> {
Box::new(Rule)
}
/// Attributes that parallel branch execution does not resolve. Per-branch
/// preambles now honor fidelity, while `thread_id` remains inert because
/// concurrent branches cannot share an LLM session.
/// Attributes that parallel branch execution does not resolve. Only
/// `thread_id` is inert on branches (concurrent branches cannot share an LLM
/// session); per-branch `fidelity` is honored via pre-rendered preambles.
const BRANCH_IGNORED_ATTRS: &[&str] = &["thread_id"];
const FULL_FIDELITY_MESSAGE: &str = "Parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session";
const THREAD_ID_FIX: &str = "Remove 'thread_id': parallel branches inherit the thread resolved when the parallel node started";
struct Rule;
/// Renders one or more parallel-node ids as `'a'` or `'a', 'b'`.
@ -22,19 +26,6 @@ fn quoted_list(ids: &[String]) -> String {
.join(", ")
}
fn fix_message(attr: &str) -> String {
match attr {
"thread_id" => format!(
"Remove '{attr}': parallel branches inherit the thread resolved when the parallel node started"
),
_ => format!("Remove '{attr}'"),
}
}
fn full_fidelity_message() -> String {
"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session".to_string()
}
fn full_fidelity_fix(parallel_ids: &[String]) -> String {
let parent = if parallel_ids.len() == 1 {
format!("parallel node {}", quoted_list(parallel_ids))
@ -46,6 +37,23 @@ fn full_fidelity_fix(parallel_ids: &[String]) -> String {
)
}
fn full_fidelity_diagnostic(
rule_name: &str,
node_id: Option<String>,
edge: Option<(String, String)>,
parallel_ids: &[String],
) -> Diagnostic {
Diagnostic {
rule: rule_name.to_string(),
severity: Severity::Warning,
message: FULL_FIDELITY_MESSAGE.to_string(),
node_id,
edge,
fix: Some(full_fidelity_fix(parallel_ids)),
..Diagnostic::default()
}
}
impl LintRule for Rule {
fn name(&self) -> &'static str {
"parallel_branch_inert_attribute"
@ -64,15 +72,12 @@ impl LintRule for Rule {
continue;
}
if edge.fidelity() == Some("full") {
diagnostics.push(Diagnostic {
rule: self.name().to_string(),
severity: Severity::Warning,
message: full_fidelity_message(),
node_id: None,
edge: Some((edge.from.clone(), edge.to.clone())),
fix: Some(full_fidelity_fix(std::slice::from_ref(&edge.from))),
..Diagnostic::default()
});
diagnostics.push(full_fidelity_diagnostic(
self.name(),
None,
Some((edge.from.clone(), edge.to.clone())),
std::slice::from_ref(&edge.from),
));
}
for attr in BRANCH_IGNORED_ATTRS {
if !edge.attrs.contains_key(*attr) {
@ -87,7 +92,7 @@ impl LintRule for Rule {
),
node_id: None,
edge: Some((edge.from.clone(), edge.to.clone())),
fix: Some(fix_message(attr)),
fix: Some(THREAD_ID_FIX.to_string()),
..Diagnostic::default()
});
}
@ -103,15 +108,12 @@ impl LintRule for Rule {
continue;
};
if node.fidelity() == Some("full") {
diagnostics.push(Diagnostic {
rule: self.name().to_string(),
severity: Severity::Warning,
message: full_fidelity_message(),
node_id: Some(node.id.clone()),
edge: None,
fix: Some(full_fidelity_fix(&parents)),
..Diagnostic::default()
});
diagnostics.push(full_fidelity_diagnostic(
self.name(),
Some(node.id.clone()),
None,
&parents,
));
}
for attr in BRANCH_IGNORED_ATTRS {
if !node.attrs.contains_key(*attr) {
@ -127,7 +129,7 @@ impl LintRule for Rule {
),
node_id: Some(node.id.clone()),
edge: None,
fix: Some(fix_message(attr)),
fix: Some(THREAD_ID_FIX.to_string()),
..Diagnostic::default()
});
}

View file

@ -23,6 +23,9 @@ impl LintRule for Rule {
let graph_default_full = graph.default_fidelity() == Some("full");
let branches = ParallelBranches::new(graph);
// thread_id is inert on parallel branches, where
// parallel_branch_inert_attribute already says "remove thread_id" —
// advising fidelity="full" there would contradict it.
for node in graph.nodes.values() {
if node.thread_id().is_some()
&& !branches.is_branch_only_node(&node.id)

View file

@ -78,12 +78,18 @@ pub fn format_artifact_reference(path: &str) -> String {
pub fn durable_context_snapshot(context: &Context) -> HashMap<String, Value> {
let mut snapshot = context.snapshot();
snapshot.remove(context::keys::CURRENT_PREAMBLE);
snapshot.remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);
strip_transient_keys(&mut snapshot);
normalize_durable_updates(&mut snapshot);
snapshot
}
/// Remove runtime-only keys that must never reach durable storage or events.
pub fn strip_transient_keys(values: &mut HashMap<String, Value>) {
for key in context::keys::TRANSIENT_CONTEXT_KEYS {
values.remove(*key);
}
}
pub fn normalize_durable_updates(updates: &mut HashMap<String, Value>) {
for value in updates.values_mut() {
normalize_durable_value(value);
@ -97,12 +103,7 @@ pub fn normalize_durable_outcomes(node_outcomes: &mut HashMap<String, Outcome>)
}
pub fn normalize_checkpoint_for_resume(checkpoint: &mut Checkpoint) {
checkpoint
.context_values
.remove(context::keys::CURRENT_PREAMBLE);
checkpoint
.context_values
.remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);
strip_transient_keys(&mut checkpoint.context_values);
normalize_durable_updates(&mut checkpoint.context_values);
normalize_durable_outcomes(&mut checkpoint.node_outcomes);
}

View file

@ -25,6 +25,9 @@ pub mod keys {
pub const INTERNAL_PARENT_PREAMBLE: &str = "internal.parent_preamble";
pub const INTERNAL_PARALLEL_GROUP_ID: &str = "internal.parallel_group_id";
pub const INTERNAL_PARALLEL_BRANCH_ID: &str = "internal.parallel_branch_id";
/// Stash of pre-rendered per-branch preambles for a parallel node; see
/// [`super::ParallelBranchPreamble`] for the entry shape and the
/// producer/consumer contract.
pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = "internal.parallel_branch_preambles";
// --- current.* keys ---
@ -45,6 +48,12 @@ pub mod keys {
pub const PARALLEL_FAN_IN_BEST_OUTCOME: &str = "parallel.fan_in.best_outcome";
pub const PARALLEL_FAN_IN_BEST_HEAD_SHA: &str = "parallel.fan_in.best_head_sha";
/// Runtime-only keys stripped from durable context projections
/// (checkpoint snapshots and resume normalization). Add new transient
/// keys here so both strip sites stay in sync.
pub const TRANSIENT_CONTEXT_KEYS: &[&str] =
&[CURRENT_PREAMBLE, INTERNAL_PARALLEL_BRANCH_PREAMBLES];
// --- Prefix constants (for filtering and dynamic keys) ---
pub const GRAPH_PREFIX: &str = "graph.";
pub const INTERNAL_PREFIX: &str = "internal.";
@ -136,9 +145,24 @@ pub mod keys {
pub use fabro_core::Context;
use fabro_graphviz::Fidelity;
use fabro_types::{ParallelBranchId, StageId};
use serde::{Deserialize, Serialize};
use crate::event::StageScope;
/// One entry of the [`keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES`] stash.
///
/// The stash is a JSON array indexed by the parallel node's outgoing-edge
/// order (`Graph::outgoing_edges` preserves declaration order, so producer and
/// consumer align even with duplicate targets). `null` entries mean the branch
/// inherits the fork's preamble. `FidelityLifecycle::before_node` produces the
/// stash; `ParallelHandler::execute` consumes and clears it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ParallelBranchPreamble {
pub fidelity: Fidelity,
pub preamble: String,
}
/// Domain-specific typed accessors for workflow context values.
pub trait WorkflowContext {
fn fidelity(&self) -> Fidelity;

View file

@ -4,14 +4,13 @@ use std::time::Instant;
use async_trait::async_trait;
use fabro_agent::{Sandbox, WorktreeOptions, WorktreeSandbox};
use fabro_graphviz::Fidelity;
use fabro_graphviz::graph::{AttrValue, Graph, Node};
use fabro_hooks::{HookContext, HookEvent};
use fabro_types::{ParallelBranchId, RunId, StageId};
use tokio::sync::Semaphore;
use super::{EngineServices, Handler};
use crate::context::{Context, WorkflowContext, keys};
use crate::context::{Context, ParallelBranchPreamble, WorkflowContext, keys};
use crate::error::Error;
use crate::event::{Event, RunNoticeCode, RunNoticeLevel, StageScope};
use crate::git::sanitize_ref_component;
@ -57,15 +56,15 @@ struct BranchResult {
worktree_path: Option<PathBuf>,
}
struct BranchPreamble {
fidelity: Fidelity,
preamble: String,
}
/// Parse the per-branch preamble stash produced by `FidelityLifecycle`.
///
/// Outer `None` means the stash is absent, malformed, or has the wrong branch
/// count — every branch then inherits the fork context (legacy behavior).
/// Inner `None` means that single branch inherits.
fn parse_branch_preambles(
value: Option<serde_json::Value>,
branch_count: usize,
) -> Option<Vec<Option<BranchPreamble>>> {
) -> Option<Vec<Option<ParallelBranchPreamble>>> {
let serde_json::Value::Array(entries) = value? else {
return None;
};
@ -77,15 +76,7 @@ fn parse_branch_preambles(
.into_iter()
.map(|entry| match entry {
serde_json::Value::Null => Some(None),
serde_json::Value::Object(entry) if entry.len() == 2 => {
let fidelity = entry.get("fidelity")?.as_str()?.parse().ok()?;
let preamble = entry.get("preamble")?.as_str()?;
Some(Some(BranchPreamble {
fidelity,
preamble: preamble.to_string(),
}))
}
_ => None,
entry => serde_json::from_value(entry).ok().map(Some),
})
.collect()
}
@ -258,6 +249,13 @@ impl Handler for ParallelHandler {
context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),
branches.len(),
);
// Clear the stash before forking so branch contexts never carry the
// outer array — a nested parallel branch target must not misread it as
// its own. The write-back diff also clears it on the run state.
context.set(
keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,
serde_json::Value::Null,
);
let mut branch_setups: Vec<BranchSetup> = Vec::new();
for (branch_index, edge) in branches.iter().enumerate() {
let target_id = edge.to.clone();
@ -279,16 +277,15 @@ impl Handler for ParallelHandler {
.and_then(|entries| entries.get(branch_index))
.and_then(Option::as_ref)
{
branch_context.set(keys::CURRENT_PREAMBLE, serde_json::json!(&entry.preamble));
branch_context.set(
keys::CURRENT_PREAMBLE,
serde_json::Value::String(entry.preamble.clone()),
);
branch_context.set(
keys::INTERNAL_FIDELITY,
serde_json::json!(entry.fidelity.to_string()),
serde_json::Value::String(entry.fidelity.to_string()),
);
}
branch_context.set(
keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,
serde_json::Value::Null,
);
let (branch_sandbox, worktree_path): (Arc<dyn Sandbox>, Option<PathBuf>) = if let (
Some(ref gs),
@ -350,10 +347,6 @@ impl Handler for ParallelHandler {
worktree_path,
});
}
context.set(
keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,
serde_json::Value::Null,
);
// --- Fan out: concurrent execution ---
let mut handles = Vec::new();

View file

@ -92,6 +92,10 @@ fn response_from_outcome(node_id: &str, outcome: &Outcome) -> Option<String> {
.and_then(|value| value.as_str().map(ToOwned::to_owned))
}
/// Context values for `StageCompleted` events. Unlike
/// `artifact::strip_transient_keys`, this keeps `CURRENT_PREAMBLE` — stage
/// events have always included the active preamble — and drops only the
/// parallel stash, which can embed every branch's rendered preamble.
fn stage_context_values(workflow_context: &Context) -> Option<BTreeMap<String, serde_json::Value>> {
let mut snapshot = workflow_context.snapshot();
snapshot.remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);

View file

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
@ -10,10 +11,10 @@ use fabro_core::state::ExecutionState;
use fabro_graphviz::graph::types::{Edge as GvEdge, Graph as GvGraph, Node as GvNode};
use crate::artifact;
use crate::context::keys;
use crate::context::{Context, ParallelBranchPreamble, keys};
use crate::graph::{WorkflowGraph, WorkflowNode};
use crate::handler::llm::preamble::build_preamble;
use crate::outcome::BilledModelUsage;
use crate::outcome::{BilledModelUsage, Outcome};
use crate::runtime_store::RunStoreHandle;
type WfRunState = ExecutionState<Option<BilledModelUsage>>;
@ -61,6 +62,60 @@ impl FidelityLifecycle {
"fidelity mutex should not be poisoned: no code panics while holding this lock",
) = flag;
}
/// Render the per-branch preamble stash for a parallel node, indexed by
/// outgoing-edge order (the same order `ParallelHandler` fans out in).
/// `Null` entries inherit the fork's preamble.
fn build_parallel_branch_preambles(
&self,
node_id: &str,
fork_fidelity: keys::Fidelity,
resolved_context: &Context,
resolved_outcomes: &HashMap<String, Outcome>,
completed_nodes: &[String],
) -> Vec<serde_json::Value> {
let mut rendered: HashMap<keys::Fidelity, serde_json::Value> = HashMap::new();
self.graph
.outgoing_edges(node_id)
.into_iter()
.enumerate()
.map(|(branch_index, edge)| {
let Some(target_node) = self.graph.nodes.get(&edge.to) else {
return serde_json::Value::Null;
};
let resolution = resolve_parallel_branch_fidelity(edge, target_node, fork_fidelity);
if resolution.requested == Some(keys::Fidelity::Full) {
tracing::warn!(
parallel_node = %node_id,
branch = %edge.to,
branch_index,
effective_fidelity = %keys::Fidelity::Full.degraded(),
"Parallel branch fidelity degraded from full"
);
}
let Some(branch_fidelity) = resolution.effective else {
return serde_json::Value::Null;
};
rendered
.entry(branch_fidelity)
.or_insert_with(|| {
let entry = ParallelBranchPreamble {
fidelity: branch_fidelity,
preamble: build_preamble(
branch_fidelity,
resolved_context,
&self.graph,
completed_nodes,
resolved_outcomes,
),
};
serde_json::to_value(entry)
.expect("ParallelBranchPreamble serialization cannot fail")
})
.clone()
})
.collect()
}
}
#[async_trait]
@ -143,47 +198,23 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {
.context
.set(keys::CURRENT_PREAMBLE, serde_json::json!(preamble));
// 5. Parallel nodes: pre-render per-branch preambles into the stash that
// ParallelHandler consumes at fan-out.
if gv_node.handler_type() == Some("parallel") {
let mut branch_preambles = Vec::new();
for (branch_index, edge) in self.graph.outgoing_edges(node.id()).iter().enumerate() {
let Some(target_node) = self.graph.nodes.get(&edge.to) else {
branch_preambles.push(serde_json::Value::Null);
continue;
};
let resolution = resolve_parallel_branch_fidelity(edge, target_node, fidelity);
if resolution.requested() == Some(keys::Fidelity::Full) {
tracing::warn!(
parallel_node = %node.id(),
branch = %edge.to,
branch_index,
fidelity = %keys::Fidelity::Full,
effective_fidelity = %keys::Fidelity::SummaryHigh,
"Parallel branch fidelity degraded"
);
}
let Some(branch_fidelity) = resolution.entry_fidelity() else {
branch_preambles.push(serde_json::Value::Null);
continue;
};
let branch_preamble = build_preamble(
branch_fidelity,
&resolved_context,
&self.graph,
&state.completed_nodes,
&resolved_outcomes,
);
branch_preambles.push(serde_json::json!({
"fidelity": branch_fidelity.to_string(),
"preamble": branch_preamble,
}));
}
let branch_preambles = self.build_parallel_branch_preambles(
node.id(),
fidelity,
&resolved_context,
&resolved_outcomes,
&state.completed_nodes,
);
state.context.set(
keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,
serde_json::Value::Array(branch_preambles),
);
}
// 5. Thread ID resolution via resolve_thread_id: edge → node → graph default →
// 6. Thread ID resolution via resolve_thread_id: edge → node → graph default →
// class → previous
let thread_id = resolve_thread_id(
incoming_edge_ref,
@ -192,13 +223,13 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {
state.previous_node_id.as_deref(),
);
// 6. Set thread.{tid}.current_node
// 7. Set thread.{tid}.current_node
if let Some(ref tid) = thread_id {
let key = keys::thread_current_node_key(tid);
state.context.set(key, serde_json::json!(node.id()));
}
// 7. Set INTERNAL_THREAD_ID (or null)
// 8. Set INTERNAL_THREAD_ID (or null)
match thread_id {
Some(tid) => {
state
@ -212,7 +243,7 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {
}
}
// 8. Set INTERNAL_NODE_VISIT_COUNT and CURRENT_NODE
// 9. Set INTERNAL_NODE_VISIT_COUNT and CURRENT_NODE
let visits = state.node_visits.get(node.id()).copied().unwrap_or(1);
state
.context
@ -243,22 +274,14 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy)]
struct ParallelBranchFidelityResolution {
/// The explicit fidelity requested on the edge or node, pre-degradation.
requested: Option<keys::Fidelity>,
/// The fidelity to render an entry for; `None` inherits the fork preamble.
effective: Option<keys::Fidelity>,
}
impl ParallelBranchFidelityResolution {
fn requested(self) -> Option<keys::Fidelity> {
self.requested
}
fn entry_fidelity(self) -> Option<keys::Fidelity> {
self.effective
}
}
/// Resolve explicit branch fidelity with edge-over-node precedence.
///
/// Branches with no explicit fidelity inherit the parallel node's preamble.
@ -270,10 +293,7 @@ fn resolve_parallel_branch_fidelity(
target_node: &GvNode,
parallel_fidelity: keys::Fidelity,
) -> ParallelBranchFidelityResolution {
let requested = edge
.fidelity()
.and_then(|value| value.parse().ok())
.or_else(|| target_node.fidelity().and_then(|value| value.parse().ok()));
let requested = explicit_fidelity(Some(edge), target_node).map(|(fidelity, _)| fidelity);
let effective = requested
.map(keys::Fidelity::degraded)
.filter(|fidelity| *fidelity != parallel_fidelity);
@ -284,6 +304,23 @@ fn resolve_parallel_branch_fidelity(
}
}
/// Explicit fidelity from the incoming edge attribute, else the node
/// attribute, with the winning source labeled for logging.
fn explicit_fidelity(
incoming_edge: Option<&GvEdge>,
node: &GvNode,
) -> Option<(keys::Fidelity, &'static str)> {
incoming_edge
.and_then(|e| e.fidelity())
.and_then(|s| s.parse().ok())
.map(|f| (f, "edge"))
.or_else(|| {
node.fidelity()
.and_then(|s| s.parse().ok())
.map(|f| (f, "node"))
})
}
/// Resolve the context fidelity for a node, following the precedence:
/// 1. Incoming edge `fidelity` attribute
/// 2. Target node `fidelity` attribute
@ -294,13 +331,8 @@ fn resolve_fidelity(
node: &GvNode,
graph: &GvGraph,
) -> keys::Fidelity {
let (resolved, source) = if let Some(f) = incoming_edge
.and_then(|e| e.fidelity())
.and_then(|s| s.parse().ok())
{
(f, "edge")
} else if let Some(f) = node.fidelity().and_then(|s| s.parse().ok()) {
(f, "node")
let (resolved, source) = if let Some((f, source)) = explicit_fidelity(incoming_edge, node) {
(f, source)
} else if let Some(f) = graph.default_fidelity().and_then(|s| s.parse().ok()) {
(f, "graph")
} else {
@ -362,7 +394,7 @@ mod tests {
use crate::context::WorkflowContext;
use crate::context::keys::Fidelity;
fn fidelity_attr(value: &str) -> AttrValue {
fn str_attr(value: &str) -> AttrValue {
AttrValue::String(value.to_string())
}
@ -374,23 +406,23 @@ mod tests {
let mut start = Node::new("start");
start
.attrs
.insert("shape".to_string(), fidelity_attr("Mdiamond"));
.insert("shape".to_string(), str_attr("Mdiamond"));
let mut fork = Node::new("fork");
fork.attrs
.insert("shape".to_string(), fidelity_attr("component"));
.insert("shape".to_string(), str_attr("component"));
if let Some(fidelity) = fork_fidelity {
fork.attrs
.insert("fidelity".to_string(), fidelity_attr(fidelity));
.insert("fidelity".to_string(), str_attr(fidelity));
}
let mut branch_a = Node::new("branch_a");
if let Some(fidelity) = branch_a_fidelity {
branch_a
.attrs
.insert("fidelity".to_string(), fidelity_attr(fidelity));
.insert("fidelity".to_string(), str_attr(fidelity));
}
let branch_b = Node::new("branch_b");
let mut work = Node::new("work");
work.attrs.insert("shape".to_string(), fidelity_attr("box"));
work.attrs.insert("shape".to_string(), str_attr("box"));
graph.nodes.insert(start.id.clone(), start);
graph.nodes.insert(fork.id.clone(), fork);
@ -426,15 +458,15 @@ mod tests {
fn parallel_branch_fidelity_edge_overrides_node() {
let mut node = Node::new("branch");
node.attrs
.insert("fidelity".to_string(), fidelity_attr("compact"));
.insert("fidelity".to_string(), str_attr("compact"));
let mut edge = Edge::new("fork", "branch");
edge.attrs
.insert("fidelity".to_string(), fidelity_attr("truncate"));
.insert("fidelity".to_string(), str_attr("truncate"));
let resolved = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);
assert_eq!(resolved.requested(), Some(Fidelity::Truncate));
assert_eq!(resolved.entry_fidelity(), Some(Fidelity::Truncate));
assert_eq!(resolved.requested, Some(Fidelity::Truncate));
assert_eq!(resolved.effective, Some(Fidelity::Truncate));
}
#[test]
@ -444,47 +476,45 @@ mod tests {
let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Compact);
assert_eq!(resolution.requested(), None);
assert_eq!(resolution.entry_fidelity(), None);
assert_eq!(resolution.requested, None);
assert_eq!(resolution.effective, None);
}
#[test]
fn parallel_branch_full_fidelity_degrades_to_summary_high() {
let mut node = Node::new("branch");
node.attrs
.insert("fidelity".to_string(), fidelity_attr("full"));
node.attrs.insert("fidelity".to_string(), str_attr("full"));
let edge = Edge::new("fork", "branch");
let resolved = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Compact);
assert_eq!(resolved.requested(), Some(Fidelity::Full));
assert_eq!(resolved.entry_fidelity(), Some(Fidelity::SummaryHigh));
assert_eq!(resolved.requested, Some(Fidelity::Full));
assert_eq!(resolved.effective, Some(Fidelity::SummaryHigh));
}
#[test]
fn parallel_branch_fidelity_equal_to_fork_inherits() {
let mut node = Node::new("branch");
node.attrs
.insert("fidelity".to_string(), fidelity_attr("summary:high"));
.insert("fidelity".to_string(), str_attr("summary:high"));
let edge = Edge::new("fork", "branch");
let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);
assert_eq!(resolution.requested(), Some(Fidelity::SummaryHigh));
assert_eq!(resolution.entry_fidelity(), None);
assert_eq!(resolution.requested, Some(Fidelity::SummaryHigh));
assert_eq!(resolution.effective, None);
}
#[test]
fn explicit_full_branch_equal_to_degraded_fork_inherits() {
let mut node = Node::new("branch");
node.attrs
.insert("fidelity".to_string(), fidelity_attr("full"));
node.attrs.insert("fidelity".to_string(), str_attr("full"));
let edge = Edge::new("fork", "branch");
let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);
assert_eq!(resolution.requested(), Some(Fidelity::Full));
assert_eq!(resolution.entry_fidelity(), None);
assert_eq!(resolution.requested, Some(Fidelity::Full));
assert_eq!(resolution.effective, None);
}
#[test]
@ -494,8 +524,8 @@ mod tests {
let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Full);
assert_eq!(resolution.requested(), None);
assert_eq!(resolution.entry_fidelity(), None);
assert_eq!(resolution.requested, None);
assert_eq!(resolution.effective, None);
}
#[tokio::test]