use crate::distill::DistilledSkill;
use crate::engine::MemgineEngine;
use crate::graph::{SkillOutcome, SkillScope, SkillTrigger};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CandidateStatus {
Candidate,
Approved,
Rejected,
Superseded,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GuidanceCandidateKind {
GeneralSkill,
TaskSkill,
FailureLesson,
AdvisorTriggerRule,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PromotionMetadata {
pub status: CandidateStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reviewed_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approved_pack_version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
impl Default for PromotionMetadata {
fn default() -> Self {
Self {
status: CandidateStatus::Candidate,
reviewed_by: None,
approved_pack_version: None,
notes: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GuidanceCandidate {
pub id: String,
pub kind: GuidanceCandidateKind,
pub title: String,
pub principle: String,
pub when_to_apply: String,
pub when_not_to_apply: String,
#[serde(default)]
pub evidence_trace_ids: Vec<String>,
pub expected_benefit: String,
#[serde(default)]
pub known_risks: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub derived_skill: Option<DistilledSkill>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub derived_trigger_rule: Option<ApprovedAdvisorTriggerRule>,
#[serde(default)]
pub promotion: PromotionMetadata,
pub created_at: DateTime<Utc>,
}
impl GuidanceCandidate {
pub fn from_distilled_skill(
id: impl Into<String>,
kind: GuidanceCandidateKind,
skill: DistilledSkill,
evidence_trace_ids: Vec<String>,
expected_benefit: impl Into<String>,
known_risks: Vec<String>,
) -> Self {
Self {
id: id.into(),
title: skill.name.clone(),
principle: skill.description.clone(),
when_to_apply: skill.when_to_apply.clone(),
when_not_to_apply: String::new(),
evidence_trace_ids,
expected_benefit: expected_benefit.into(),
known_risks,
derived_skill: Some(skill),
derived_trigger_rule: None,
promotion: PromotionMetadata::default(),
created_at: Utc::now(),
kind,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApprovedSkill {
pub id: String,
pub title: String,
pub description: String,
pub when_to_apply: String,
pub when_not_to_apply: String,
pub scope: SkillScope,
pub trigger: SkillTrigger,
#[serde(default)]
pub code: String,
#[serde(default)]
pub source_candidate_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ApprovedFailureLesson {
pub id: String,
pub title: String,
pub description: String,
pub when_to_apply: String,
pub when_not_to_apply: String,
#[serde(default)]
pub source_candidate_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ApprovedAdvisorTriggerRule {
pub id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repeated_failure_threshold: Option<u32>,
#[serde(default)]
pub require_high_risk: bool,
#[serde(default)]
pub require_missing_guidance: bool,
#[serde(default)]
pub source_candidate_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApprovedSkillPack {
pub version: u32,
#[serde(default)]
pub general_skills: Vec<ApprovedSkill>,
#[serde(default)]
pub task_specific_skills: HashMap<String, Vec<ApprovedSkill>>,
#[serde(default)]
pub common_mistakes: Vec<ApprovedFailureLesson>,
#[serde(default)]
pub advisor_trigger_rules: Vec<ApprovedAdvisorTriggerRule>,
pub updated_at: DateTime<Utc>,
}
impl ApprovedSkillPack {
pub fn empty(version: u32) -> Self {
Self {
version,
general_skills: Vec::new(),
task_specific_skills: HashMap::new(),
common_mistakes: Vec::new(),
advisor_trigger_rules: Vec::new(),
updated_at: Utc::now(),
}
}
pub fn materialize_into_memgine(&self, engine: &mut MemgineEngine) -> Vec<String> {
let mut loaded = Vec::new();
for skill in &self.general_skills {
ingest_approved_skill(engine, skill);
loaded.push(skill.id.clone());
}
for skills in self.task_specific_skills.values() {
for skill in skills {
ingest_approved_skill(engine, skill);
loaded.push(skill.id.clone());
}
}
loaded
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeSkillOverlay {
#[serde(default)]
pub loaded_skill_ids: Vec<String>,
#[serde(default)]
pub skill_stats: HashMap<String, RuntimeSkillStats>,
#[serde(default)]
pub review_queue: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeSkillStats {
pub success_count: u64,
pub fail_count: u64,
pub needs_review: bool,
}
impl RuntimeSkillOverlay {
pub fn new(loaded_skill_ids: Vec<String>) -> Self {
Self {
loaded_skill_ids,
skill_stats: HashMap::new(),
review_queue: Vec::new(),
}
}
pub fn record_outcome(&mut self, skill_id: &str, outcome: SkillOutcome) -> &RuntimeSkillStats {
let stats = self.skill_stats.entry(skill_id.to_string()).or_default();
match outcome {
SkillOutcome::Success => stats.success_count += 1,
SkillOutcome::Fail => stats.fail_count += 1,
}
stats.needs_review = stats.fail_count > stats.success_count + 2;
if stats.needs_review && !self.review_queue.iter().any(|s| s == skill_id) {
self.review_queue.push(skill_id.to_string());
}
self.skill_stats
.get(skill_id)
.expect("overlay stats just inserted")
}
}
#[derive(Debug, Default)]
pub struct GovernedMaterialization {
pub loaded: Vec<String>,
pub refused: Vec<(String, String)>,
pub pending: Vec<(String, String)>,
}
impl ApprovedSkillPack {
pub fn materialize_into_memgine_governed(
&self,
engine: &mut MemgineEngine,
provenance: &car_policy::skill_trust::SkillProvenance,
requested: car_policy::permission::PermissionTier,
ledger: &car_policy::permission::ApprovalLedger,
) -> GovernedMaterialization {
let mut out = GovernedMaterialization::default();
let all = self
.general_skills
.iter()
.chain(self.task_specific_skills.values().flatten());
for skill in all {
let res = engine.ingest_skill_governed_full(
&skill.title,
&skill.code,
"approved_pack",
skill.trigger.clone(),
&skill.description,
skill.scope.clone(),
&skill.when_to_apply,
None,
vec![],
vec![],
provenance.clone(),
requested,
ledger,
);
if res.ingested.is_some() {
out.loaded.push(skill.id.clone());
} else if let Some(p) = &res.enforcement.pending {
out.pending.push((skill.id.clone(), p.fingerprint.clone()));
} else {
out.refused
.push((skill.id.clone(), res.enforcement.reason.clone()));
}
}
out
}
}
fn ingest_approved_skill(engine: &mut MemgineEngine, skill: &ApprovedSkill) {
engine.ingest_skill_full(
&skill.title,
&skill.code,
"approved_pack",
skill.trigger.clone(),
&skill.description,
skill.scope.clone(),
&skill.when_to_apply,
None,
vec![],
vec![],
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn candidate_from_distilled_skill_keeps_trace_refs() {
let skill = DistilledSkill {
name: "verify_before_edit".to_string(),
description: "Check current file state before mutating.".to_string(),
when_to_apply: "Before writing or patching files".to_string(),
scope: SkillScope::Global,
source: "success".to_string(),
domain: String::new(),
trigger: SkillTrigger::default(),
code: String::new(),
};
let candidate = GuidanceCandidate::from_distilled_skill(
"cand-1",
GuidanceCandidateKind::GeneralSkill,
skill,
vec!["trajectory:t1:event:2".to_string()],
"reduce bad edits",
vec!["may be overcautious".to_string()],
);
assert_eq!(candidate.id, "cand-1");
assert_eq!(
candidate.evidence_trace_ids,
vec!["trajectory:t1:event:2".to_string()]
);
assert_eq!(candidate.promotion.status, CandidateStatus::Candidate);
}
#[test]
fn approved_pack_materializes_into_memgine() {
let mut engine = MemgineEngine::new(None);
let mut pack = ApprovedSkillPack::empty(1);
pack.general_skills.push(ApprovedSkill {
id: "skill-1".to_string(),
title: "verify_before_edit".to_string(),
description: "Check state before editing".to_string(),
when_to_apply: "Before write operations".to_string(),
when_not_to_apply: String::new(),
scope: SkillScope::Global,
trigger: SkillTrigger::default(),
code: String::new(),
source_candidate_ids: vec!["cand-1".to_string()],
});
let loaded = pack.materialize_into_memgine(&mut engine);
let found = engine.find_skill("", "", "Before write operations", 5);
assert_eq!(loaded, vec!["skill-1".to_string()]);
assert!(found
.iter()
.any(|(meta, _)| meta.name == "verify_before_edit"));
}
#[test]
fn runtime_overlay_tracks_outcomes_without_mutating_pack() {
let mut overlay = RuntimeSkillOverlay::new(vec!["skill-1".to_string()]);
overlay.record_outcome("skill-1", SkillOutcome::Fail);
overlay.record_outcome("skill-1", SkillOutcome::Fail);
overlay.record_outcome("skill-1", SkillOutcome::Fail);
let stats = overlay.skill_stats.get("skill-1").unwrap();
assert_eq!(stats.fail_count, 3);
assert!(stats.needs_review);
assert_eq!(overlay.review_queue, vec!["skill-1".to_string()]);
}
}
#[cfg(test)]
mod governed_tests {
use super::*;
use car_policy::permission::{ApprovalLedger, PermissionTier};
use car_policy::skill_trust::{SkillProvenance, SkillSource};
fn pack_with_one_skill() -> ApprovedSkillPack {
let mut pack = ApprovedSkillPack::empty(1);
pack.general_skills.push(ApprovedSkill {
id: "s1".to_string(),
title: "check_before_edit".to_string(),
description: "verify file state first".to_string(),
when_to_apply: "before edits".to_string(),
when_not_to_apply: String::new(),
scope: SkillScope::Global,
trigger: SkillTrigger::default(),
code: "…".to_string(),
source_candidate_ids: vec![],
});
pack
}
#[test]
fn governed_pack_load_deploys_trusted_and_stamps_ceiling() {
let mut e = MemgineEngine::new(None);
let prov = SkillProvenance {
signed: true,
signer_trusted: true,
scanned: true,
source: SkillSource::Official,
..Default::default()
};
let out = pack_with_one_skill().materialize_into_memgine_governed(
&mut e,
&prov,
PermissionTier::FullAccess,
&ApprovalLedger::new(),
);
assert_eq!(out.loaded, vec!["s1".to_string()]);
assert!(out.refused.is_empty() && out.pending.is_empty());
let meta = e.skill_meta("check_before_edit").expect("deployed");
assert_eq!(meta.deployment_tier, Some(PermissionTier::FullAccess));
assert_eq!(
meta.when_to_apply, "before edits",
"full shape survives the gate"
);
}
#[test]
fn governed_pack_load_refuses_vulnerable_skill_entirely() {
let mut e = MemgineEngine::new(None);
let prov = SkillProvenance {
signed: true,
signer_trusted: true,
scanned: true,
vulnerabilities: 2,
source: SkillSource::Official,
..Default::default()
};
let out = pack_with_one_skill().materialize_into_memgine_governed(
&mut e,
&prov,
PermissionTier::ReadOnly,
&ApprovalLedger::new(),
);
assert!(out.loaded.is_empty());
assert!(
e.skill_meta("check_before_edit").is_none(),
"denied skill never enters the graph"
);
assert_eq!(out.pending.len() + out.refused.len(), 1);
}
}