use super::support::{
find_guardrail, has_data_access_capability, is_mutation, is_read_only,
ARTIFACT_WRITE_GUARDRAIL_NAME, DEFAULT_ARTIFACT_SCOPE, RENDER_CONTRACT_CAPABILITY,
};
use super::types::RenderFlavor;
use crate::ir::ComponentNode;
use crate::provider::ToolMap;
use crate::resolve::ResolutionReport;
use crate::targets::CapabilityOutcome;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum GateKind {
Realize,
Degrade,
None,
}
pub(super) struct RenderGate {
pub(super) kind: GateKind,
pub(super) scope: Option<String>,
pub(super) flavor: Option<RenderFlavor>,
}
pub(super) fn resolve_render_gate(
node: &ComponentNode,
report: &ResolutionReport,
flavor: RenderFlavor,
) -> RenderGate {
let artifact_write = find_guardrail(&node.guardrails, ARTIFACT_WRITE_GUARDRAIL_NAME);
let Some(artifact_write) = artifact_write else {
return RenderGate {
kind: GateKind::None,
scope: None,
flavor: None,
};
};
if node.effect.render_blocks.is_empty() {
return RenderGate {
kind: GateKind::None,
scope: None,
flavor: None,
};
}
let render_entry = report
.iter()
.find(|r| r.capability == RENDER_CONTRACT_CAPABILITY);
match render_entry.map(|e| e.outcome) {
Some(CapabilityOutcome::RealizeVia) => RenderGate {
kind: GateKind::Realize,
scope: Some(
artifact_write
.scope
.clone()
.unwrap_or_else(|| DEFAULT_ARTIFACT_SCOPE.to_string()),
),
flavor: Some(flavor),
},
Some(CapabilityOutcome::Degrade) => RenderGate {
kind: GateKind::Degrade,
scope: None,
flavor: None,
},
_ => RenderGate {
kind: GateKind::None,
scope: None,
flavor: None,
},
}
}
pub(super) fn gate_grants_write(gate: &RenderGate) -> bool {
gate.kind == GateKind::Realize && gate.flavor == Some(RenderFlavor::Prompt)
}
pub(super) fn build_tools(
node: &ComponentNode,
gate: &RenderGate,
tool_map: &ToolMap,
) -> Vec<String> {
let mut tools: Vec<String> = vec!["Read".to_string()];
if has_data_access_capability(&node.required_capabilities) || is_mutation(node) {
tools.push("Bash(wren:*)".to_string());
}
if requires_blast_radius_gate(node) {
tools.push("Bash(warble:*)".to_string());
}
for capability in &node.required_capabilities {
if let Some(binding) = tool_map.get(capability.as_str()) {
tools.extend(binding.names.iter().cloned());
}
}
let mutating = !is_read_only(&node.guardrails);
if mutating {
tools.push("Edit".to_string());
}
if mutating || gate_grants_write(gate) {
tools.push("Write".to_string());
}
tools
}
fn requires_blast_radius_gate(node: &ComponentNode) -> bool {
node.required_capabilities
.iter()
.any(|c| c == "blast_radius")
|| find_guardrail(&node.guardrails, "blast_radius_limit").is_some()
}
pub(super) fn build_description(node: &ComponentNode) -> String {
if let Some(authored) = authored_description(node) {
return authored;
}
synthesized_description(node)
}
pub(super) fn authored_description(node: &ComponentNode) -> Option<String> {
let purpose = node.description.as_deref()?.trim();
if purpose.is_empty() {
return None;
}
if node.examples.is_empty() {
return Some(purpose.to_string());
}
Some(format!(
"{purpose} Examples: {}",
node.examples
.iter()
.map(|example| format!("\"{}\"", example.trim()))
.collect::<Vec<_>>()
.join("; ")
))
}
pub(super) fn synthesized_description(node: &ComponentNode) -> String {
if node.context_binding.absent {
return "Follow the authored instructions for this component.".to_string();
}
let block_types: Vec<&str> = node
.effect
.render_blocks
.iter()
.map(|b| b.block_type.as_str())
.collect();
let blocks = if block_types.is_empty() {
"no render blocks".to_string()
} else {
block_types.join(", ")
};
format!(
"{} {} that renders {blocks} (outcome: {}).",
node.component_type.as_str(),
node.realization_kind.as_str(),
node.effect.outcome.kind.as_str()
)
}