use crate::guidance::inventory;
use crate::guidance::types::{ArtifactRef, ArtifactState, CandidateAction, CandidateStatus};
use crate::guidance::types::{GuidanceCandidate, GuidanceScoreRow};
use anyhow::{Result, anyhow};
use std::fs;
use std::path::{Path, PathBuf};
pub fn deterministic(
row: &GuidanceScoreRow,
now_ms: u64,
rejected: &[GuidanceCandidate],
) -> GuidanceCandidate {
let intended = intended_action(row);
let blocked = was_rejected(&intended, rejected);
let action = if blocked {
CandidateAction::ReviewOnly
} else {
intended
};
GuidanceCandidate {
id: uuid::Uuid::now_v7().to_string(),
artifact: row.artifact.clone(),
action,
status: CandidateStatus::Proposed,
rationale: rationale(row, blocked),
evidence: evidence(row, rejected),
created_at_ms: now_ms,
applied_at_ms: None,
treatment_fingerprint: None,
experiment_id: None,
backup_path: None,
}
}
pub fn rejected_memory_lines(rejected: &[GuidanceCandidate]) -> Vec<String> {
rejected
.iter()
.take(5)
.map(|c| format!("{} {}: {}", c.id, action_label(&c.action), c.rationale))
.collect()
}
pub fn apply_candidate(
workspace: &Path,
candidate: &GuidanceCandidate,
now_ms: u64,
) -> Result<AppliedCandidate> {
let artifact = inventory::find(workspace, &candidate.artifact)?;
let backup = backup_artifact(workspace, &candidate.id, &artifact.path)?;
match &candidate.action {
CandidateAction::Delete => delete_artifact(&candidate.artifact, &artifact.path)?,
CandidateAction::Replace { content } => fs::write(&artifact.path, content)?,
CandidateAction::ReviewOnly => return Err(anyhow!("candidate has no applyable edit")),
}
let snapshot = crate::prompt::snapshot::capture(workspace, now_ms)?;
Ok(AppliedCandidate {
backup_path: backup.to_string_lossy().to_string(),
treatment_fingerprint: snapshot.fingerprint.clone(),
snapshot,
})
}
pub struct AppliedCandidate {
pub backup_path: String,
pub treatment_fingerprint: String,
pub snapshot: crate::prompt::PromptSnapshot,
}
fn intended_action(row: &GuidanceScoreRow) -> CandidateAction {
match row.state {
ArtifactState::Stale => CandidateAction::Delete,
_ => CandidateAction::ReviewOnly,
}
}
fn rationale(row: &GuidanceScoreRow, blocked: bool) -> String {
if blocked {
return "prior equivalent candidate rejected; review manually".into();
}
match row.state {
ArtifactState::Stale => "artifact has no observed sessions in window".into(),
ArtifactState::Current => format!(
"artifact score {:.1}; review evidence before editing",
row.score
),
ArtifactState::InsufficientEvidence => "not enough sessions for validation".into(),
}
}
fn evidence(row: &GuidanceScoreRow, rejected: &[GuidanceCandidate]) -> Vec<String> {
row.evidence
.iter()
.cloned()
.chain(rejected_memory_lines(rejected))
.collect()
}
fn was_rejected(action: &CandidateAction, rejected: &[GuidanceCandidate]) -> bool {
rejected.iter().any(|c| same_action(action, &c.action))
}
fn same_action(a: &CandidateAction, b: &CandidateAction) -> bool {
match (a, b) {
(CandidateAction::Delete, CandidateAction::Delete) => true,
(CandidateAction::ReviewOnly, CandidateAction::ReviewOnly) => true,
(CandidateAction::Replace { content: x }, CandidateAction::Replace { content: y }) => {
x == y
}
_ => false,
}
}
pub fn action_label(action: &CandidateAction) -> &'static str {
match action {
CandidateAction::Delete => "delete",
CandidateAction::Replace { .. } => "replace",
CandidateAction::ReviewOnly => "review_only",
}
}
fn backup_artifact(workspace: &Path, id: &str, path: &Path) -> Result<PathBuf> {
let rel = path.strip_prefix(workspace).unwrap_or(path);
let backup = workspace.join(".kaizen/backup/guidance").join(id).join(rel);
if let Some(parent) = backup.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(path, &backup)?;
Ok(backup)
}
fn delete_artifact(artifact: &ArtifactRef, path: &Path) -> Result<()> {
fs::remove_file(path)?;
if artifact.kind == crate::guidance::types::ArtifactKind::Skill {
remove_empty_parent(path)?;
}
Ok(())
}
fn remove_empty_parent(path: &Path) -> Result<()> {
if let Some(parent) = path.parent()
&& parent.read_dir()?.next().is_none()
{
fs::remove_dir(parent)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::guidance::{
ArtifactKind, ArtifactRef, ArtifactState, CandidateStatus, GuidanceCandidate,
};
#[test]
fn rejected_delete_blocks_repeated_delete() {
let c = deterministic(&row(), 1, &[candidate(CandidateAction::Delete)]);
assert_eq!(c.action, CandidateAction::ReviewOnly);
assert!(c.rationale.contains("rejected"));
}
fn row() -> GuidanceScoreRow {
GuidanceScoreRow {
artifact: ArtifactRef {
kind: ArtifactKind::Rule,
slug: "dead".into(),
},
path: "dead".into(),
state: ArtifactState::Stale,
score: 0.0,
sessions: 0,
avg_cost_usd: None,
mean_eval_score: None,
bad_feedback: 0,
failed_outcomes: 0,
tool_loops: 0,
train: Default::default(),
validation: Default::default(),
generalization_gap: None,
validation_gate: Default::default(),
evidence: vec![],
}
}
fn candidate(action: CandidateAction) -> GuidanceCandidate {
GuidanceCandidate {
id: "c1".into(),
artifact: row().artifact,
action,
status: CandidateStatus::Proposed,
rationale: "unused".into(),
evidence: vec![],
created_at_ms: 0,
applied_at_ms: None,
treatment_fingerprint: None,
experiment_id: None,
backup_path: None,
}
}
}