use std::collections::HashMap;
use crate::brain::decisions::{DecisionRecord, read_all_decisions};
use crate::brain::outcomes::{ResolvedOutcome, load_resolved_map};
use crate::brain::preferences::{
DistilledPreferences, PreferencePattern, TemporalPattern, ToolAccuracy,
};
use super::{KnowledgeContent, KnowledgeScope, KnowledgeUnit, epoch_secs, gen_ku_id, semantic_key};
pub struct ExportThresholds {
pub min_pattern_evidence: u32,
pub min_tool_decisions: u32,
pub min_temporal_strength: f64,
pub min_outcome_samples: u32,
pub min_cluster_variant_evidence: u32,
}
impl Default for ExportThresholds {
fn default() -> Self {
Self {
min_pattern_evidence: 5,
min_tool_decisions: 10,
min_temporal_strength: 0.7,
min_outcome_samples: 5,
min_cluster_variant_evidence: 3,
}
}
}
pub fn distill_to_knowledge(
prefs: &DistilledPreferences,
source_peer: &str,
project: Option<&str>,
thresholds: &ExportThresholds,
) -> Vec<KnowledgeUnit> {
let now = epoch_secs();
let scope = match project {
Some(p) => KnowledgeScope::Project(p.to_string()),
None => KnowledgeScope::Universal,
};
let mut units = Vec::new();
for pattern in &prefs.patterns {
if let Some(unit) = pattern_to_unit(pattern, source_peer, &scope, now, thresholds) {
units.push(unit);
}
}
for acc in &prefs.tool_accuracy {
if let Some(unit) = accuracy_to_unit(acc, source_peer, now, thresholds) {
units.push(unit);
}
}
for tp in &prefs.temporal {
if let Some(unit) = temporal_to_unit(tp, source_peer, &scope, now, thresholds) {
units.push(unit);
}
}
let _ = crate::brain::outcomes::reap();
let decisions = read_all_decisions();
let resolved = load_resolved_map();
units.extend(distill_outcomes(
&decisions,
&resolved,
source_peer,
&scope,
project,
now,
thresholds,
));
units.extend(detect_clusters(prefs, source_peer, &scope, now, thresholds));
units
}
pub fn distill_to_knowledge_stable(
prefs: &DistilledPreferences,
source_peer: &str,
project: Option<&str>,
thresholds: &ExportThresholds,
existing: &super::store::HiveStore,
) -> Vec<KnowledgeUnit> {
let mut units = distill_to_knowledge(prefs, source_peer, project, thresholds);
for unit in &mut units {
let sk = semantic_key(unit);
if let Some(existing_unit) = existing.find_by_semantic_key(&sk) {
if existing_unit.source_peer == unit.source_peer {
unit.id = existing_unit.id.clone();
unit.version = existing_unit.version + 1;
unit.originated_at = existing_unit.originated_at;
unit.propagation_count = existing_unit.propagation_count;
unit.injection_state = existing_unit.injection_state;
unit.injection_stats = existing_unit.injection_stats.clone();
}
} else {
unit.injection_state = super::InjectionState::Canary;
}
}
units
}
fn approach_ref_for(decision: &DecisionRecord) -> Option<String> {
let tool = decision.tool.as_deref()?;
let cmd = decision
.command
.as_deref()
.map(|s| s.split_whitespace().collect::<Vec<_>>().join(" "))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "*".to_string());
Some(format!("pattern:{tool}:{cmd}"))
}
#[derive(Default)]
struct OutcomeBucket {
samples: u32,
successes: u32,
costs: Vec<f64>,
durations_ms: Vec<u64>,
project_hits: HashMap<String, u32>,
}
fn median_f64(mut v: Vec<f64>) -> Option<f64> {
if v.is_empty() {
return None;
}
v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = v.len() / 2;
Some(if v.len() % 2 == 0 {
(v[mid - 1] + v[mid]) / 2.0
} else {
v[mid]
})
}
fn median_u64(mut v: Vec<u64>) -> Option<u64> {
if v.is_empty() {
return None;
}
v.sort();
let mid = v.len() / 2;
Some(if v.len() % 2 == 0 {
(v[mid - 1] + v[mid]) / 2
} else {
v[mid]
})
}
pub fn distill_outcomes(
decisions: &[DecisionRecord],
resolved: &HashMap<String, ResolvedOutcome>,
source_peer: &str,
scope: &KnowledgeScope,
project_filter: Option<&str>,
now: u64,
thresholds: &ExportThresholds,
) -> Vec<KnowledgeUnit> {
let mut buckets: HashMap<String, OutcomeBucket> = HashMap::new();
for d in decisions {
if let Some(p) = project_filter {
if !d.project.eq_ignore_ascii_case(p) {
continue;
}
}
let Some(decision_id) = d.decision_id.as_deref() else {
continue;
};
let Some(outcome) = resolved.get(decision_id) else {
continue;
};
let Some(key) = approach_ref_for(d) else {
continue;
};
let entry = buckets.entry(key).or_default();
entry.samples += 1;
if outcome.exit_code.unwrap_or(-1) == 0 {
entry.successes += 1;
}
if let Some(ctx) = &d.context {
if ctx.cost_usd > 0.0 {
entry.costs.push(ctx.cost_usd);
}
}
if let Some(ms) = outcome.duration_ms {
entry.durations_ms.push(ms);
}
*entry.project_hits.entry(d.project.clone()).or_insert(0) += 1;
}
let mut out = Vec::new();
for (approach_ref, bucket) in buckets {
if bucket.samples < thresholds.min_outcome_samples {
continue;
}
let success_rate = bucket.successes as f64 / bucket.samples as f64;
let median_cost_usd = median_f64(bucket.costs);
let median_duration_ms = median_u64(bucket.durations_ms);
let mut conditions: Vec<String> = bucket
.project_hits
.into_iter()
.filter(|(_, n)| *n >= 2)
.map(|(p, n)| format!("project:{p} (n={n})"))
.collect();
conditions.sort();
out.push(KnowledgeUnit {
id: gen_ku_id(),
scope: scope.clone(),
category: super::KnowledgeCategory::BestPractice,
content: KnowledgeContent::ApproachOutcome {
approach_ref,
success_rate,
sample_count: bucket.samples,
median_cost_usd,
median_duration_ms,
conditions,
},
evidence_count: bucket.samples,
confidence: success_rate,
source_peer: source_peer.to_string(),
originated_at: now,
last_validated_at: now,
propagation_count: 0,
version: 1,
revalidation_interval_secs: 0,
injection_state: crate::hive::InjectionState::Live,
injection_stats: crate::hive::InjectionStats {
injected_count: 0,
accepted_count: 0,
overridden_count: 0,
last_injected_at: 0,
last_outcome_at: 0,
},
sharing_consent: None,
});
}
out
}
fn problem_key_for(pattern: &PreferencePattern) -> String {
let cmd = pattern.command_pattern.as_deref().unwrap_or("*");
let normalized = cmd.split_whitespace().collect::<Vec<_>>().join(" ");
format!("{}:{normalized}", pattern.tool)
}
fn condition_labels(pattern: &PreferencePattern) -> Vec<String> {
pattern.conditions.iter().map(|c| c.label()).collect()
}
pub fn detect_clusters(
prefs: &DistilledPreferences,
source_peer: &str,
scope: &KnowledgeScope,
now: u64,
thresholds: &ExportThresholds,
) -> Vec<KnowledgeUnit> {
let mut groups: HashMap<String, Vec<&PreferencePattern>> = HashMap::new();
for p in &prefs.patterns {
groups.entry(problem_key_for(p)).or_default().push(p);
}
let mut units = Vec::new();
for (problem_key, patterns) in groups {
if patterns.len() < 2 {
continue;
}
let qualifying: Vec<&PreferencePattern> = patterns
.into_iter()
.filter(|p| p.sample_count >= thresholds.min_cluster_variant_evidence)
.collect();
if qualifying.len() < 2 {
continue;
}
let mut actions: Vec<&str> = qualifying
.iter()
.map(|p| p.preferred_action.as_str())
.collect();
actions.sort();
actions.dedup();
if actions.len() < 2 {
continue; }
let outcome_ref = format!("pattern:{problem_key}");
let variants: Vec<super::ApproachVariant> = qualifying
.iter()
.map(|p| super::ApproachVariant {
approach_summary: format!(
"{} ({:.0}% accept over {})",
p.preferred_action,
p.accept_rate * 100.0,
p.sample_count
),
conditions: condition_labels(p),
evidence: p.sample_count,
contributing_peers: vec![source_peer.to_string()],
outcome_ref: Some(outcome_ref.clone()),
})
.collect();
let total_evidence: u32 = variants.iter().map(|v| v.evidence).sum();
let mean_confidence =
qualifying.iter().map(|p| p.confidence).sum::<f64>() / qualifying.len() as f64;
units.push(KnowledgeUnit {
id: gen_ku_id(),
scope: scope.clone(),
category: super::KnowledgeCategory::Technique,
content: KnowledgeContent::ApproachCluster {
problem_key,
variants,
},
evidence_count: total_evidence,
confidence: mean_confidence,
source_peer: source_peer.to_string(),
originated_at: now,
last_validated_at: now,
propagation_count: 0,
version: 1,
revalidation_interval_secs: 0,
injection_state: crate::hive::InjectionState::Live,
injection_stats: crate::hive::InjectionStats {
injected_count: 0,
accepted_count: 0,
overridden_count: 0,
last_injected_at: 0,
last_outcome_at: 0,
},
sharing_consent: None,
});
}
units
}
fn classify_pattern(pattern: &PreferencePattern) -> super::KnowledgeCategory {
use super::KnowledgeCategory;
use crate::brain::preferences::PreferenceCondition;
for cond in &pattern.conditions {
match cond {
PreferenceCondition::CostAbove(_) | PreferenceCondition::CostBelow(_) => {
return KnowledgeCategory::Personal;
}
PreferenceCondition::HourRange(_, _) => {
return KnowledgeCategory::Personal;
}
_ => {}
}
}
KnowledgeCategory::BestPractice
}
fn pattern_to_unit(
pattern: &PreferencePattern,
source_peer: &str,
scope: &KnowledgeScope,
now: u64,
thresholds: &ExportThresholds,
) -> Option<KnowledgeUnit> {
if pattern.sample_count < thresholds.min_pattern_evidence {
return None;
}
let category = classify_pattern(pattern);
let conditions: Vec<String> = pattern.conditions.iter().map(|c| c.label()).collect();
Some(KnowledgeUnit {
id: gen_ku_id(),
scope: scope.clone(),
category,
content: KnowledgeContent::Pattern {
tool: pattern.tool.clone(),
command_pattern: pattern.command_pattern.clone(),
preferred_action: pattern.preferred_action.clone(),
accept_rate: pattern.accept_rate,
sample_count: pattern.sample_count,
conditions,
},
evidence_count: pattern.sample_count,
confidence: pattern.accept_rate,
source_peer: source_peer.to_string(),
originated_at: now,
last_validated_at: now,
propagation_count: 0,
version: 1,
revalidation_interval_secs: 0,
injection_state: crate::hive::InjectionState::Live,
injection_stats: crate::hive::InjectionStats {
injected_count: 0,
accepted_count: 0,
overridden_count: 0,
last_injected_at: 0,
last_outcome_at: 0,
},
sharing_consent: None,
})
}
fn accuracy_to_unit(
acc: &ToolAccuracy,
source_peer: &str,
now: u64,
thresholds: &ExportThresholds,
) -> Option<KnowledgeUnit> {
if acc.total < thresholds.min_tool_decisions {
return None;
}
Some(KnowledgeUnit {
id: gen_ku_id(),
scope: KnowledgeScope::Universal,
category: super::KnowledgeCategory::BestPractice,
content: KnowledgeContent::ToolAccuracy {
tool: acc.tool.clone(),
total: acc.total,
correct: acc.correct,
confidence_threshold: acc.confidence_threshold,
},
evidence_count: acc.total,
confidence: if acc.total > 0 {
acc.correct as f64 / acc.total as f64
} else {
0.0
},
source_peer: source_peer.to_string(),
originated_at: now,
last_validated_at: now,
propagation_count: 0,
version: 1,
revalidation_interval_secs: 0,
injection_state: crate::hive::InjectionState::Live,
injection_stats: crate::hive::InjectionStats {
injected_count: 0,
accepted_count: 0,
overridden_count: 0,
last_injected_at: 0,
last_outcome_at: 0,
},
sharing_consent: None,
})
}
fn classify_temporal(tp: &TemporalPattern) -> super::KnowledgeCategory {
use super::KnowledgeCategory;
let desc = tp.description.to_lowercase();
if desc.contains("hour")
|| desc.contains("time of day")
|| desc.contains("morning")
|| desc.contains("evening")
|| desc.contains("approval")
{
return KnowledgeCategory::Personal;
}
if desc.contains("cost") || desc.contains("spend") || desc.contains("budget") {
return KnowledgeCategory::Personal;
}
if desc.contains("error") || desc.contains("context") || desc.contains("retry") {
return KnowledgeCategory::Technique;
}
KnowledgeCategory::BestPractice
}
fn temporal_to_unit(
tp: &TemporalPattern,
source_peer: &str,
scope: &KnowledgeScope,
now: u64,
thresholds: &ExportThresholds,
) -> Option<KnowledgeUnit> {
if tp.strength < thresholds.min_temporal_strength {
return None;
}
let category = classify_temporal(tp);
Some(KnowledgeUnit {
id: gen_ku_id(),
scope: scope.clone(),
category,
content: KnowledgeContent::Temporal {
description: tp.description.clone(),
strength: tp.strength,
},
evidence_count: tp.sample_count,
confidence: tp.strength,
source_peer: source_peer.to_string(),
originated_at: now,
last_validated_at: now,
propagation_count: 0,
version: 1,
revalidation_interval_secs: 0,
injection_state: crate::hive::InjectionState::Live,
injection_stats: crate::hive::InjectionStats {
injected_count: 0,
accepted_count: 0,
overridden_count: 0,
last_injected_at: 0,
last_outcome_at: 0,
},
sharing_consent: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_prefs() -> DistilledPreferences {
DistilledPreferences {
patterns: vec![
PreferencePattern {
tool: "Bash".into(),
command_pattern: Some("cargo test".into()),
preferred_action: "approve".into(),
sample_count: 20,
accept_rate: 0.95,
conditions: vec![],
confidence: 0.95,
},
PreferencePattern {
tool: "Bash".into(),
command_pattern: Some("rm -rf".into()),
preferred_action: "deny".into(),
sample_count: 3, accept_rate: 0.0,
conditions: vec![],
confidence: 0.0,
},
],
tool_accuracy: vec![
ToolAccuracy {
tool: "Bash".into(),
total: 50,
correct: 42,
confidence_threshold: 0.7,
},
ToolAccuracy {
tool: "Read".into(),
total: 5, correct: 5,
confidence_threshold: 0.5,
},
],
temporal: vec![
TemporalPattern {
description: "Error streak detected".into(),
sample_count: 8,
strength: 0.85,
},
TemporalPattern {
description: "Weak pattern".into(),
sample_count: 3,
strength: 0.3, },
],
total_decisions: 100,
overall_accuracy: 84.0,
}
}
#[test]
fn distill_filters_by_thresholds() {
let prefs = make_prefs();
let units = distill_to_knowledge(&prefs, "test-peer", None, &ExportThresholds::default());
assert_eq!(units.len(), 3);
}
#[test]
fn pattern_below_threshold_excluded() {
let prefs = make_prefs();
let units = distill_to_knowledge(&prefs, "test-peer", None, &ExportThresholds::default());
let pattern_tools: Vec<&str> = units
.iter()
.filter_map(|u| match &u.content {
KnowledgeContent::Pattern { tool, .. } => Some(tool.as_str()),
_ => None,
})
.collect();
assert_eq!(pattern_tools, vec!["Bash"]);
}
#[test]
fn accuracy_below_threshold_excluded() {
let prefs = make_prefs();
let units = distill_to_knowledge(&prefs, "test-peer", None, &ExportThresholds::default());
let accuracy_tools: Vec<&str> = units
.iter()
.filter_map(|u| match &u.content {
KnowledgeContent::ToolAccuracy { tool, .. } => Some(tool.as_str()),
_ => None,
})
.collect();
assert_eq!(accuracy_tools, vec!["Bash"]);
}
#[test]
fn temporal_below_threshold_excluded() {
let prefs = make_prefs();
let units = distill_to_knowledge(&prefs, "test-peer", None, &ExportThresholds::default());
let temporal_descs: Vec<&str> = units
.iter()
.filter_map(|u| match &u.content {
KnowledgeContent::Temporal { description, .. } => Some(description.as_str()),
_ => None,
})
.collect();
assert_eq!(temporal_descs, vec!["Error streak detected"]);
}
#[test]
fn project_scope_applied() {
let prefs = make_prefs();
let units = distill_to_knowledge(
&prefs,
"test-peer",
Some("claudectl"),
&ExportThresholds::default(),
);
for unit in &units {
match &unit.content {
KnowledgeContent::ToolAccuracy { .. } => {
assert_eq!(unit.scope, KnowledgeScope::Universal);
}
_ => {
assert_eq!(unit.scope, KnowledgeScope::Project("claudectl".into()));
}
}
}
}
#[test]
fn stable_distill_reuses_ids() {
let prefs = make_prefs();
let first = distill_to_knowledge(&prefs, "test-peer", None, &ExportThresholds::default());
let mut store =
super::super::store::HiveStore::load_from(std::path::Path::new("/nonexistent"));
for unit in &first {
store.insert(unit.clone());
}
let second = distill_to_knowledge_stable(
&prefs,
"test-peer",
None,
&ExportThresholds::default(),
&store,
);
for unit in &second {
let sk = semantic_key(unit);
if let Some(original) = store.find_by_semantic_key(&sk) {
assert_eq!(unit.id, original.id);
assert_eq!(unit.version, original.version + 1);
}
}
}
fn make_decision_with_id(id: &str, tool: &str, command: &str, project: &str) -> DecisionRecord {
use crate::brain::decisions::DecisionType;
DecisionRecord {
timestamp: "0".into(),
pid: 1,
project: project.into(),
tool: Some(tool.into()),
command: Some(command.into()),
brain_action: "approve".into(),
brain_confidence: 0.9,
brain_reasoning: "test".into(),
user_action: "accept".into(),
context: None,
outcome: None,
decision_type: DecisionType::Session,
suggested_at: None,
resolved_at: None,
override_reason: None,
decision_id: Some(id.into()),
}
}
fn make_resolved(
id: &str,
tool: &str,
project: &str,
exit: Option<i32>,
dur: Option<u64>,
) -> ResolvedOutcome {
ResolvedOutcome {
decision_id: id.into(),
tool: tool.into(),
command: Some("cargo test".into()),
project: project.into(),
exit_code: exit,
duration_ms: dur,
stderr_tail: None,
ts: 0,
}
}
#[test]
fn distill_outcomes_aggregates_per_approach() {
let decisions = vec![
make_decision_with_id("d1", "Bash", "cargo test", "p"),
make_decision_with_id("d2", "Bash", "cargo test", "p"),
make_decision_with_id("d3", "Bash", "cargo test", "p"),
];
let mut resolved = HashMap::new();
resolved.insert(
"d1".into(),
make_resolved("d1", "Bash", "p", Some(0), Some(100)),
);
resolved.insert(
"d2".into(),
make_resolved("d2", "Bash", "p", Some(0), Some(200)),
);
resolved.insert(
"d3".into(),
make_resolved("d3", "Bash", "p", Some(1), Some(300)),
);
let units = distill_outcomes(
&decisions,
&resolved,
"peer",
&KnowledgeScope::Universal,
None,
0,
&ExportThresholds {
min_outcome_samples: 1,
..Default::default()
},
);
assert_eq!(units.len(), 1, "all three share the same approach_ref");
match &units[0].content {
KnowledgeContent::ApproachOutcome {
approach_ref,
success_rate,
sample_count,
median_duration_ms,
..
} => {
assert_eq!(approach_ref, "pattern:Bash:cargo test");
assert_eq!(*sample_count, 3);
assert!((success_rate - 2.0 / 3.0).abs() < 1e-9);
assert_eq!(*median_duration_ms, Some(200));
}
other => panic!("unexpected content {other:?}"),
}
}
#[test]
fn distill_outcomes_threshold_filters() {
let decisions = vec![make_decision_with_id("d1", "Bash", "ls", "p")];
let mut resolved = HashMap::new();
resolved.insert("d1".into(), make_resolved("d1", "Bash", "p", Some(0), None));
let units = distill_outcomes(
&decisions,
&resolved,
"peer",
&KnowledgeScope::Universal,
None,
0,
&ExportThresholds {
min_outcome_samples: 5,
..Default::default()
},
);
assert!(units.is_empty(), "1 sample below threshold of 5");
}
#[test]
fn distill_outcomes_skips_decisions_without_id() {
let mut d = make_decision_with_id("d1", "Bash", "cargo test", "p");
d.decision_id = None;
let resolved = HashMap::new();
let units = distill_outcomes(
&[d],
&resolved,
"peer",
&KnowledgeScope::Universal,
None,
0,
&ExportThresholds {
min_outcome_samples: 1,
..Default::default()
},
);
assert!(units.is_empty());
}
#[test]
fn distill_outcomes_project_filter() {
let decisions = vec![
make_decision_with_id("d1", "Bash", "cargo test", "alpha"),
make_decision_with_id("d2", "Bash", "cargo test", "beta"),
];
let mut resolved = HashMap::new();
resolved.insert(
"d1".into(),
make_resolved("d1", "Bash", "alpha", Some(0), None),
);
resolved.insert(
"d2".into(),
make_resolved("d2", "Bash", "beta", Some(0), None),
);
let units = distill_outcomes(
&decisions,
&resolved,
"peer",
&KnowledgeScope::Project("alpha".into()),
Some("alpha"),
0,
&ExportThresholds {
min_outcome_samples: 1,
..Default::default()
},
);
assert_eq!(units.len(), 1);
if let KnowledgeContent::ApproachOutcome { sample_count, .. } = &units[0].content {
assert_eq!(*sample_count, 1, "beta decision should be filtered out");
} else {
panic!("wrong content variant");
}
}
fn make_pattern(
tool: &str,
cmd: &str,
action: &str,
n: u32,
accept: f64,
conds: Vec<crate::brain::preferences::PreferenceCondition>,
) -> PreferencePattern {
PreferencePattern {
tool: tool.into(),
command_pattern: Some(cmd.into()),
preferred_action: action.into(),
sample_count: n,
accept_rate: accept,
conditions: conds,
confidence: accept,
}
}
#[test]
fn detect_clusters_on_divergent_actions() {
use crate::brain::preferences::PreferenceCondition;
let prefs = DistilledPreferences {
patterns: vec![
make_pattern(
"Bash",
"git push",
"approve",
8,
0.9,
vec![PreferenceCondition::CostBelow(1.0)],
),
make_pattern(
"Bash",
"git push",
"deny",
5,
0.1,
vec![PreferenceCondition::CostAbove(1.0)],
),
],
tool_accuracy: vec![],
temporal: vec![],
total_decisions: 13,
overall_accuracy: 0.0,
};
let units = detect_clusters(
&prefs,
"peer-a",
&KnowledgeScope::Universal,
0,
&ExportThresholds::default(),
);
assert_eq!(
units.len(),
1,
"should produce one cluster for the divergent group"
);
if let KnowledgeContent::ApproachCluster {
problem_key,
variants,
} = &units[0].content
{
assert_eq!(problem_key, "Bash:git push");
assert_eq!(variants.len(), 2);
let summaries: Vec<&str> = variants
.iter()
.map(|v| v.approach_summary.as_str())
.collect();
assert!(summaries.iter().any(|s| s.starts_with("approve")));
assert!(summaries.iter().any(|s| s.starts_with("deny")));
} else {
panic!("wrong content variant");
}
}
#[test]
fn detect_clusters_skips_when_actions_agree() {
let prefs = DistilledPreferences {
patterns: vec![
make_pattern("Bash", "ls", "approve", 8, 0.9, vec![]),
make_pattern("Bash", "ls", "approve", 5, 0.95, vec![]),
],
tool_accuracy: vec![],
temporal: vec![],
total_decisions: 13,
overall_accuracy: 0.0,
};
let units = detect_clusters(
&prefs,
"peer-a",
&KnowledgeScope::Universal,
0,
&ExportThresholds::default(),
);
assert!(units.is_empty(), "no divergence → no cluster");
}
#[test]
fn detect_clusters_skips_below_per_variant_threshold() {
let prefs = DistilledPreferences {
patterns: vec![
make_pattern("Bash", "rm -rf", "deny", 8, 0.0, vec![]),
make_pattern("Bash", "rm -rf", "approve", 1, 1.0, vec![]),
],
tool_accuracy: vec![],
temporal: vec![],
total_decisions: 9,
overall_accuracy: 0.0,
};
let units = detect_clusters(
&prefs,
"peer-a",
&KnowledgeScope::Universal,
0,
&ExportThresholds::default(),
);
assert!(
units.is_empty(),
"weak counter-pattern should not promote a cluster"
);
}
#[test]
fn approach_outcome_semantic_key_stable_across_peers() {
let unit_a = KnowledgeUnit {
id: gen_ku_id(),
scope: KnowledgeScope::Universal,
category: super::super::KnowledgeCategory::BestPractice,
content: KnowledgeContent::ApproachOutcome {
approach_ref: "pattern:Bash:cargo test".into(),
success_rate: 0.9,
sample_count: 10,
median_cost_usd: None,
median_duration_ms: None,
conditions: vec![],
},
evidence_count: 10,
confidence: 0.9,
source_peer: "peer-a".into(),
originated_at: 0,
last_validated_at: 0,
propagation_count: 0,
version: 1,
revalidation_interval_secs: 0,
injection_state: crate::hive::InjectionState::Live,
injection_stats: crate::hive::InjectionStats {
injected_count: 0,
accepted_count: 0,
overridden_count: 0,
last_injected_at: 0,
last_outcome_at: 0,
},
sharing_consent: None,
};
let mut unit_b = unit_a.clone();
unit_b.source_peer = "peer-b".into();
unit_b.id = gen_ku_id();
assert_eq!(semantic_key(&unit_a), semantic_key(&unit_b));
}
#[test]
fn custom_thresholds() {
let prefs = make_prefs();
let strict = ExportThresholds {
min_pattern_evidence: 50,
min_tool_decisions: 100,
min_temporal_strength: 0.99,
min_outcome_samples: u32::MAX,
min_cluster_variant_evidence: u32::MAX,
};
let units = distill_to_knowledge(&prefs, "test-peer", None, &strict);
assert_eq!(units.len(), 0); }
}