use car_eventlog::harness_adapt::{HarnessIntervention, InterventionLayer};
use serde::{Deserialize, Serialize};
use super::ab::{AbReport, RoundAttribution};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProposalKind {
HarnessAddressable,
BackboneBound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Confidence {
Strong,
Tentative,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DurableFixProposal {
pub title: String,
pub pattern: String,
pub target_component: String,
pub target_hint: String,
pub proposed_change: String,
pub evidence_tasks: Vec<String>,
pub evidence_count: usize,
pub kind: ProposalKind,
pub confidence: Confidence,
pub priority: u32,
}
fn proposal_from_intervention(iv: &HarnessIntervention) -> DurableFixProposal {
let trig = iv.trigger.to_lowercase();
let (component, hint, change) = match iv.layer {
InterventionLayer::EnvironmentContract => (
"coder tool contract / system prompt",
"coder/native_loop.rs (system_prompt) or the tool descriptions",
format!(
"The model repeatedly proposes a call the runtime rejects for '{}'. Durably clarify \
the tool's description/constraints (or the prompt's guidance about it) so the model \
stops proposing the disallowed/ill-formed call up front.",
iv.target
),
),
InterventionLayer::ActionRealization => (
"tool schema / prompt tool-call format",
"car-inference render_chat_prompt/parse_tool_calls or coder/native_loop.rs prompt",
format!(
"The model emits structurally-invalid calls to '{}'. Durably fix the tool-call \
rendering/parsing or tighten the prompt's format guidance so valid calls are the \
default, rather than relying on retries.",
iv.target
),
),
InterventionLayer::TrajectoryRegulation => {
if trig.contains("retried") || trig.contains("failed") {
(
"coder repair discipline",
"coder/native_loop.rs (system_prompt 'read the actual error before retrying' + failure_feedback)",
format!(
"'{}' fails/retries repeatedly without recovering. Durably strengthen the \
read-the-error-then-fix-the-named-cause discipline (prompt + the failure \
feedback the repair loop injects), so the coder converges instead of \
thrashing.",
iv.target
),
)
} else {
(
"coder repair loop / iteration handling",
"coder/native_loop.rs (max_iterations, failure_feedback) or the replan path",
format!(
"Repair for '{}' gives up (replanning exhausted). Durably improve how the \
coder carries context across repair rounds (or reconsiders the approach) so \
it doesn't burn the budget re-deriving the same dead end.",
iv.target
),
)
}
}
InterventionLayer::ProceduralSkill => (
"skill distillation",
"car-memgine skill distillation / coder skill_memory",
format!(
"A reusable procedure keeps being re-derived for '{}'. Durably distill it as a coder \
skill so future sessions recall it.",
iv.target
),
),
};
let evidence_count = iv.evidence_count;
DurableFixProposal {
title: format!("Fix {component} for recurring failure on '{}'", iv.target),
pattern: iv.trigger.clone(),
target_component: component.to_string(),
target_hint: hint.to_string(),
proposed_change: change,
evidence_tasks: Vec::new(), evidence_count,
kind: ProposalKind::HarnessAddressable,
confidence: Confidence::Strong,
priority: 100 + evidence_count as u32,
}
}
fn backbone_bound_proposal(tasks: &[String]) -> DurableFixProposal {
DurableFixProposal {
title: format!(
"Inspect {} clean-but-wrong loss(es) for a common error class",
tasks.len()
),
pattern: "coder ran cleanly but produced a wrong answer (no diagnosable interaction failure)"
.to_string(),
target_component: "coder system prompt (task discipline)".to_string(),
target_hint: "coder/native_loop.rs (system_prompt)".to_string(),
proposed_change:
"Read these failing cases and name the common mistake (e.g. losing collection order, \
off-by-one, wrong edge case). If they share one, add targeted pitfall guidance to the \
coder's system prompt — the same move that took a corpus 55%→100% by warning against \
set() dropping order. If they don't share one, this is a backbone limit, not a harness \
fix — record it and move on."
.to_string(),
evidence_tasks: tasks.to_vec(),
evidence_count: tasks.len(),
kind: ProposalKind::BackboneBound,
confidence: Confidence::Tentative,
priority: 40 + tasks.len() as u32,
}
}
pub fn synthesize_proposals(
_report: &AbReport,
attribution: &RoundAttribution,
) -> Vec<DurableFixProposal> {
let mut proposals: Vec<DurableFixProposal> = attribution
.interventions
.iter()
.map(proposal_from_intervention)
.collect();
if !attribution.backbone_bound_losses.is_empty() {
proposals.push(backbone_bound_proposal(&attribution.backbone_bound_losses));
}
proposals.sort_by(|a, b| {
b.priority
.cmp(&a.priority)
.then(b.evidence_count.cmp(&a.evidence_count))
.then(a.title.cmp(&b.title))
});
proposals
}
pub fn render_proposals(proposals: &[DurableFixProposal]) -> String {
if proposals.is_empty() {
return "No durable-fix proposals — no attributable coder losses this round.".to_string();
}
let mut out = format!("Durable code-fix proposals ({}):\n", proposals.len());
for (i, p) in proposals.iter().enumerate() {
out.push_str(&format!(
"\n{}. [{:?}, {:?}, priority {}] {}\n pattern: {}\n target: {} — {}\n change: {}\n",
i + 1,
p.kind,
p.confidence,
p.priority,
p.title,
p.pattern,
p.target_component,
p.target_hint,
p.proposed_change,
));
if !p.evidence_tasks.is_empty() {
out.push_str(&format!(
" evidence: {} task(s): {}\n",
p.evidence_count,
p.evidence_tasks.join(", ")
));
} else {
out.push_str(&format!(" evidence: recurrence {}\n", p.evidence_count));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use car_eventlog::harness_adapt::HarnessIntervention;
fn iv(layer: InterventionLayer, target: &str, trigger: &str, ev: usize) -> HarnessIntervention {
HarnessIntervention {
layer,
target: target.into(),
trigger: trigger.into(),
intervention: "x".into(),
evidence_count: ev,
}
}
fn attribution(
interventions: Vec<HarnessIntervention>,
backbone: Vec<&str>,
) -> RoundAttribution {
RoundAttribution {
harness_addressable_losses: vec![],
backbone_bound_losses: backbone.into_iter().map(String::from).collect(),
interventions,
}
}
fn empty_report() -> AbReport {
AbReport::from_cells(vec![], None)
}
#[test]
fn trajectory_retry_maps_to_repair_discipline() {
let attr = attribution(
vec![iv(
InterventionLayer::TrajectoryRegulation,
"run_command",
"action 'run_command' retried 4×",
4,
)],
vec![],
);
let ps = synthesize_proposals(&empty_report(), &attr);
assert_eq!(ps.len(), 1);
assert_eq!(ps[0].kind, ProposalKind::HarnessAddressable);
assert!(ps[0].target_hint.contains("native_loop.rs"));
assert!(ps[0].proposed_change.contains("read-the-error"));
}
#[test]
fn realization_maps_to_tool_rendering() {
let attr = attribution(
vec![iv(
InterventionLayer::ActionRealization,
"edit_file",
"no tool 'edit_file'",
3,
)],
vec![],
);
let ps = synthesize_proposals(&empty_report(), &attr);
assert!(
ps[0].target_hint.contains("render_chat_prompt")
|| ps[0].target_hint.contains("parse_tool_calls")
);
}
#[test]
fn backbone_bound_cluster_targets_the_prompt_with_the_55_to_100_precedent() {
let attr = attribution(vec![], vec!["dedup", "sort_stable"]);
let ps = synthesize_proposals(&empty_report(), &attr);
assert_eq!(ps.len(), 1);
let p = &ps[0];
assert_eq!(p.kind, ProposalKind::BackboneBound);
assert_eq!(p.confidence, Confidence::Tentative);
assert_eq!(p.evidence_tasks, vec!["dedup", "sort_stable"]);
assert!(p.proposed_change.contains("55%→100%"));
}
#[test]
fn harness_addressable_outranks_backbone_bound() {
let attr = attribution(
vec![iv(
InterventionLayer::TrajectoryRegulation,
"t",
"action 't' failed 2×",
2,
)],
vec!["a", "b", "c", "d", "e"],
);
let ps = synthesize_proposals(&empty_report(), &attr);
assert_eq!(ps.len(), 2);
assert_eq!(ps[0].kind, ProposalKind::HarnessAddressable);
assert_eq!(ps[1].kind, ProposalKind::BackboneBound);
}
#[test]
fn empty_attribution_yields_no_proposals() {
let ps = synthesize_proposals(&empty_report(), &attribution(vec![], vec![]));
assert!(ps.is_empty());
assert!(render_proposals(&ps).contains("No durable-fix proposals"));
}
#[test]
fn render_is_human_readable() {
let attr = attribution(
vec![iv(
InterventionLayer::EnvironmentContract,
"shell",
"action 'shell' rejected 3×",
3,
)],
vec!["x"],
);
let text = render_proposals(&synthesize_proposals(&empty_report(), &attr));
assert!(text.contains("Durable code-fix proposals (2)"));
assert!(text.contains("target:"));
assert!(text.contains("change:"));
}
}