use std::path::Path;
use crate::coder::{ContractCheck, OutcomeContract};
pub(crate) struct TemplateContext<'a> {
pub dedup_key: &'a str,
pub detector_id: &'a str,
pub locator: &'a str,
pub tool: &'a str,
pub params_path: &'a Path,
}
#[derive(Debug)]
pub(crate) struct RenderedTemplate {
pub intent: String,
pub contract: OutcomeContract,
}
type Renderer = fn(&TemplateContext<'_>) -> Result<RenderedTemplate, String>;
struct TemplateRegistration {
detector_id: &'static str,
render: Renderer,
}
const REGISTRY: &[TemplateRegistration] = &[TemplateRegistration {
detector_id: car_selfheal::detectors::tools::DETECTOR_ID,
render: render_recurring_tool_failure,
}];
pub(crate) fn render(context: &TemplateContext<'_>) -> Result<RenderedTemplate, String> {
let registration = REGISTRY
.iter()
.find(|registration| registration.detector_id == context.detector_id)
.ok_or_else(|| {
format!(
"self-heal detector '{}' has no auto-fix template",
context.detector_id
)
})?;
(registration.render)(context)
}
fn render_recurring_tool_failure(
context: &TemplateContext<'_>,
) -> Result<RenderedTemplate, String> {
validate_tool_name(context.tool)?;
let params_path = shell_path(context.params_path);
let tool_crate = tool_crate(context.tool);
let intent = format!(
"selfheal: repair recurring {} builtin failure\n\n\
Repair the CAR builtin tool failure identified by self-heal key {}. \
Detector locator: {}. Preserve the supplied outcome contract exactly; \
do not weaken, replace, or remove any check. The runtime evaluates the \
contract independently. Changing, adding, or weakening the `car tools call` \
replay verb or its contract assertion is out of bounds.",
context.tool, context.dedup_key, context.locator
);
let contract = OutcomeContract {
allow_credentials: false,
description: format!(
"The recurring builtin '{}' call succeeds and its owning crate remains green.",
context.tool
),
checks: vec![
ContractCheck {
name: "build_car_cli".to_string(),
command: "cd car-rs && cargo build -p car-cli".to_string(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 600,
baseline: false,
differential: None,
},
ContractCheck {
name: "replay_failed_tool_call".to_string(),
command: format!(
"car tools call {} --params-file {params_path}",
context.tool
),
expect_exit_zero: true,
output_contains: Some("$json:/ok=true".to_string()),
timeout_secs: 120,
baseline: false,
differential: None,
},
ContractCheck {
name: "tool_crate_tests".to_string(),
command: format!("cd car-rs && cargo test -p {tool_crate}"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 600,
baseline: false,
differential: None,
},
],
};
let issues = contract.validate();
if !issues.is_empty() {
return Err(format!(
"self-heal template '{}' produced an invalid contract: {}",
context.detector_id,
issues.join("; ")
));
}
Ok(RenderedTemplate { intent, contract })
}
fn tool_crate(_tool: &str) -> &'static str {
"car-engine"
}
fn validate_tool_name(tool: &str) -> Result<(), String> {
if !tool.is_empty()
&& tool
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
{
Ok(())
} else {
Err(format!(
"self-heal reconstructed an unsafe builtin tool name '{tool}'"
))
}
}
#[cfg(unix)]
fn shell_path(path: &Path) -> String {
format!("'{}'", path.to_string_lossy().replace('\'', "'\\''"))
}
#[cfg(windows)]
fn shell_path(path: &Path) -> String {
format!("\"{}\"", path.to_string_lossy().replace('"', "\"\""))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recurring_template_contract_commands_are_registry_owned_and_exact() {
let params = Path::new("/tmp/call.json");
let rendered = render(&TemplateContext {
dedup_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
detector_id: car_selfheal::detectors::tools::DETECTOR_ID,
locator: "calculate|tool_error",
tool: "calculate",
params_path: params,
})
.unwrap();
assert!(rendered.intent.starts_with("selfheal: "));
assert!(rendered
.intent
.contains("runtime evaluates the contract independently"));
assert!(rendered
.intent
.contains("Changing, adding, or weakening the `car tools call` replay verb"));
#[cfg(unix)]
let replay = "car tools call calculate --params-file '/tmp/call.json'";
#[cfg(windows)]
let replay = "car tools call calculate --params-file \"/tmp/call.json\"";
assert_eq!(
rendered
.contract
.checks
.iter()
.map(|check| check.command.as_str())
.collect::<Vec<_>>(),
[
"cd car-rs && cargo build -p car-cli",
replay,
"cd car-rs && cargo test -p car-engine",
]
);
assert!(rendered.contract.checks[0]
.command
.starts_with("cd car-rs && cargo "));
assert!(rendered.contract.checks[2]
.command
.starts_with("cd car-rs && cargo "));
assert_eq!(
rendered.contract.checks[1].output_contains.as_deref(),
Some("$json:/ok=true")
);
}
#[test]
fn permissive_replay_verb_cannot_turn_tool_failure_green() {
let rendered = render(&TemplateContext {
dedup_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
detector_id: car_selfheal::detectors::tools::DETECTOR_ID,
locator: "read_file|tool_error",
tool: "read_file",
params_path: Path::new("params.json"),
})
.unwrap();
let replay = &rendered.contract.checks[1];
assert!(!crate::coder::contract::check_assertions(
replay,
Some(0),
r#"{"ok":false,"error":"still failed"}"#,
false,
));
assert!(crate::coder::contract::check_assertions(
replay,
Some(0),
r#"{"ok":true,"result":{}}"#,
false,
));
}
#[test]
fn registry_refuses_untemplated_detector() {
assert!(render(&TemplateContext {
dedup_key: "key",
detector_id: car_selfheal::detectors::metrics::DETECTOR_ID,
locator: "metric",
tool: "calculate",
params_path: Path::new("params.json"),
})
.unwrap_err()
.contains("no auto-fix template"));
}
}