#![allow(dead_code)]
pub mod accept;
pub mod archive;
pub mod cli;
pub mod convergence;
pub mod discovery;
pub mod distiller;
pub mod effectiveness;
pub mod exposure;
pub mod feedback;
#[cfg(feature = "relay")]
pub mod gossip;
pub mod injection;
pub mod merger;
pub mod store;
pub mod trust;
use std::sync::atomic::{AtomicU64, Ordering};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KnowledgeCategory {
BestPractice,
Technique,
WorkflowPattern,
Personal,
}
impl KnowledgeCategory {
pub fn is_shareable(&self) -> bool {
!matches!(self, Self::Personal)
}
pub fn is_allowed_by(&self, allow_list: &[String]) -> bool {
if !self.is_shareable() {
return false;
}
if allow_list.is_empty() {
return true; }
allow_list.iter().any(|s| s == self.label())
}
pub fn label(&self) -> &'static str {
match self {
Self::BestPractice => "best_practice",
Self::Technique => "technique",
Self::WorkflowPattern => "workflow",
Self::Personal => "personal",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeUnit {
pub id: String,
pub scope: KnowledgeScope,
#[serde(default = "default_category")]
pub category: KnowledgeCategory,
pub content: KnowledgeContent,
pub evidence_count: u32,
pub confidence: f64,
pub source_peer: String,
pub originated_at: u64,
pub last_validated_at: u64,
pub propagation_count: u32,
pub version: u32,
#[serde(default)]
pub revalidation_interval_secs: u64,
#[serde(default)]
pub injection_state: InjectionState,
#[serde(default)]
pub injection_stats: InjectionStats,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sharing_consent: Option<SharingConsent>,
}
fn default_category() -> KnowledgeCategory {
KnowledgeCategory::BestPractice
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum MinTrustTier {
Confirmed,
Suggested,
Unverified,
#[default]
Any,
}
impl MinTrustTier {
fn rank(&self) -> u8 {
match self {
Self::Confirmed => 4,
Self::Suggested => 3,
Self::Unverified => 2,
Self::Any => 1,
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Confirmed => "confirmed",
Self::Suggested => "suggested",
Self::Unverified => "unverified",
Self::Any => "any",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"confirmed" => Some(Self::Confirmed),
"suggested" => Some(Self::Suggested),
"unverified" => Some(Self::Unverified),
"any" => Some(Self::Any),
_ => None,
}
}
pub fn admits_rank(&self, peer_rank: u8) -> bool {
peer_rank >= self.rank()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SharingConsent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_peers: Option<std::collections::HashSet<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclude_peers: Option<std::collections::HashSet<String>>,
#[serde(default = "default_min_tier")]
pub min_trust_tier: MinTrustTier,
}
fn default_min_tier() -> MinTrustTier {
MinTrustTier::Any
}
impl SharingConsent {
pub fn allows(&self, target_peer: &str, target_rank: u8, now: u64) -> bool {
if let Some(deadline) = self.expires_at {
if now > deadline {
return false;
}
}
if let Some(ex) = &self.exclude_peers {
if ex.contains(target_peer) {
return false;
}
}
if let Some(allow) = &self.allow_peers {
if !allow.contains(target_peer) {
return false;
}
}
if !self.min_trust_tier.admits_rank(target_rank) {
return false;
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum InjectionState {
Draft,
Canary,
Staged,
#[default]
Live,
}
impl InjectionState {
pub fn label(&self) -> &'static str {
match self {
Self::Draft => "draft",
Self::Canary => "canary",
Self::Staged => "staged",
Self::Live => "live",
}
}
pub fn sample_buckets(&self) -> u8 {
match self {
Self::Draft => 0,
Self::Canary => 1,
Self::Staged => 5,
Self::Live => 10,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InjectionStats {
#[serde(default)]
pub injected_count: u64,
#[serde(default)]
pub accepted_count: u64,
#[serde(default)]
pub overridden_count: u64,
#[serde(default)]
pub last_injected_at: u64,
#[serde(default)]
pub last_outcome_at: u64,
}
impl InjectionStats {
pub fn win_rate(&self) -> f64 {
let decided = self.accepted_count + self.overridden_count;
if decided == 0 {
return 0.0;
}
self.accepted_count as f64 / decided as f64
}
pub fn decided(&self) -> u64 {
self.accepted_count + self.overridden_count
}
}
pub fn default_revalidation_interval(content: &KnowledgeContent) -> u64 {
const DAY: u64 = 86_400;
match content {
KnowledgeContent::Pattern { .. }
| KnowledgeContent::ToolAccuracy { .. }
| KnowledgeContent::PromotedRule { .. }
| KnowledgeContent::ApproachCluster { .. } => 30 * DAY,
KnowledgeContent::ApproachOutcome { .. } => 14 * DAY,
KnowledgeContent::Temporal { .. } | KnowledgeContent::Insight { .. } => 7 * DAY,
KnowledgeContent::Skill { .. }
| KnowledgeContent::Command { .. }
| KnowledgeContent::HookConfig { .. } => 90 * DAY,
}
}
pub fn effective_confidence(unit: &KnowledgeUnit, now: u64) -> f64 {
let interval = if unit.revalidation_interval_secs == 0 {
default_revalidation_interval(&unit.content)
} else {
unit.revalidation_interval_secs
};
if interval == 0 {
return unit.confidence;
}
let age = now.saturating_sub(unit.last_validated_at);
if age <= interval {
return unit.confidence;
}
let overdue_intervals = (age - 1) / interval;
const MAX_DECAY_ROUNDS: u32 = 50;
let n = overdue_intervals.min(MAX_DECAY_ROUNDS as u64) as i32;
unit.confidence * 0.9_f64.powi(n)
}
pub fn is_stale(unit: &KnowledgeUnit, now: u64) -> bool {
let interval = if unit.revalidation_interval_secs == 0 {
default_revalidation_interval(&unit.content)
} else {
unit.revalidation_interval_secs
};
if interval == 0 {
return false;
}
now.saturating_sub(unit.last_validated_at) > interval
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type", content = "value")]
pub enum KnowledgeScope {
Universal,
Language(String),
Project(String),
}
pub const MAX_SKILL_BYTES: usize = 32 * 1024;
pub const MAX_COMMAND_BYTES: usize = 16 * 1024;
pub const MAX_HOOK_CONFIG_BYTES: usize = 4 * 1024;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ArtifactRequires {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cli: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub os: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum KnowledgeContent {
Pattern {
tool: String,
command_pattern: Option<String>,
preferred_action: String,
accept_rate: f64,
sample_count: u32,
conditions: Vec<String>,
},
ToolAccuracy {
tool: String,
total: u32,
correct: u32,
confidence_threshold: f64,
},
Temporal { description: String, strength: f64 },
Insight {
category: String,
severity: String,
summary: String,
suggestion: Option<String>,
},
PromotedRule { rule: String, source_type: String },
Skill {
name: String,
description: String,
version: String,
body: String,
#[serde(default)]
requires: ArtifactRequires,
},
Command {
name: String,
description: String,
args: Option<String>,
body: String,
#[serde(default)]
requires: ArtifactRequires,
},
HookConfig {
event: String,
matcher: String,
description: String,
config_json: String,
#[serde(default)]
requires: ArtifactRequires,
},
ApproachOutcome {
approach_ref: String,
success_rate: f64,
sample_count: u32,
#[serde(default)]
median_cost_usd: Option<f64>,
#[serde(default)]
median_duration_ms: Option<u64>,
#[serde(default)]
conditions: Vec<String>,
},
ApproachCluster {
problem_key: String,
variants: Vec<ApproachVariant>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApproachVariant {
pub approach_summary: String,
#[serde(default)]
pub conditions: Vec<String>,
pub evidence: u32,
#[serde(default)]
pub contributing_peers: Vec<String>,
#[serde(default)]
pub outcome_ref: Option<String>,
}
fn truncate_chars(s: &str, max_chars: usize) -> &str {
match s.char_indices().nth(max_chars) {
Some((idx, _)) => &s[..idx],
None => s,
}
}
pub fn semantic_key(unit: &KnowledgeUnit) -> String {
let scope_part = match &unit.scope {
KnowledgeScope::Universal => "universal".to_string(),
KnowledgeScope::Language(l) => format!("lang:{l}"),
KnowledgeScope::Project(p) => format!("proj:{p}"),
};
let content_part = match &unit.content {
KnowledgeContent::Pattern {
tool,
command_pattern,
..
} => {
let cmd = command_pattern.as_deref().unwrap_or("*");
format!("pattern:{tool}:{cmd}")
}
KnowledgeContent::ToolAccuracy { tool, .. } => format!("accuracy:{tool}"),
KnowledgeContent::Temporal { description, .. } => {
format!("temporal:{}", truncate_chars(description, 40))
}
KnowledgeContent::Insight {
category, summary, ..
} => {
format!("insight:{category}:{}", truncate_chars(summary, 40))
}
KnowledgeContent::PromotedRule { rule, .. } => {
format!("rule:{}", truncate_chars(rule, 40))
}
KnowledgeContent::Skill { name, .. } => {
format!("skill:{}", name.to_lowercase().replace(' ', "-"))
}
KnowledgeContent::Command { name, .. } => {
format!("command:{}", name.to_lowercase())
}
KnowledgeContent::HookConfig { event, matcher, .. } => {
format!("hook:{}:{}", event.to_lowercase(), matcher.to_lowercase())
}
KnowledgeContent::ApproachOutcome { approach_ref, .. } => {
format!("outcome:{approach_ref}")
}
KnowledgeContent::ApproachCluster { problem_key, .. } => {
format!("cluster:{problem_key}")
}
};
format!("{scope_part}/{content_part}")
}
#[derive(Debug, Clone, Default)]
pub struct SharingFilter {
pub allow_categories: Vec<String>,
pub exclude_tools: Vec<String>,
pub exclude_commands: Vec<String>,
pub exclude_content_types: Vec<String>,
}
impl SharingFilter {
pub fn from_config(cfg: &crate::config::HiveConfig) -> Self {
SharingFilter {
allow_categories: cfg.share_categories.clone(),
exclude_tools: cfg.exclude_tools.clone(),
exclude_commands: cfg.exclude_commands.clone(),
exclude_content_types: cfg.exclude_content_types.clone(),
}
}
pub fn allows(&self, unit: &KnowledgeUnit) -> bool {
if !unit.category.is_allowed_by(&self.allow_categories) {
return false;
}
if let KnowledgeContent::Pattern {
ref tool,
ref command_pattern,
..
} = unit.content
{
if self.exclude_tools.iter().any(|t| t == tool) {
return false;
}
if let Some(cmd) = command_pattern {
if self
.exclude_commands
.iter()
.any(|exc| cmd.contains(exc.as_str()))
{
return false;
}
}
}
if let KnowledgeContent::ToolAccuracy { ref tool, .. } = unit.content {
if self.exclude_tools.iter().any(|t| t == tool) {
return false;
}
}
match &unit.content {
KnowledgeContent::Skill { .. }
if self.exclude_content_types.iter().any(|t| t == "skill") =>
{
return false;
}
KnowledgeContent::Command { name, .. } => {
if self.exclude_content_types.iter().any(|t| t == "command") {
return false;
}
if self
.exclude_commands
.iter()
.any(|exc| name.contains(exc.as_str()))
{
return false;
}
}
KnowledgeContent::HookConfig { .. }
if self.exclude_content_types.iter().any(|t| t == "hook") =>
{
return false;
}
_ => {}
}
true
}
}
static KU_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn gen_ku_id() -> String {
let epoch = epoch_secs();
let seq = KU_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("ku_{epoch}_{seq}")
}
pub fn epoch_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub fn mode_path() -> std::path::PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
std::path::PathBuf::from(home)
.join(".claudectl")
.join("hive")
.join("mode")
}
pub fn read_mode_override() -> Option<String> {
let path = mode_path();
let raw = std::fs::read_to_string(&path).ok()?;
let trimmed = raw.trim().to_lowercase();
if trimmed == "on" || trimmed == "off" {
Some(trimmed)
} else {
None
}
}
pub fn write_mode_override(mode: &str) -> std::io::Result<()> {
let path = mode_path();
if mode == "clear" {
if path.exists() {
std::fs::remove_file(&path)?;
}
return Ok(());
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, mode)
}
pub fn is_active(cfg: Option<&crate::config::HiveConfig>) -> bool {
match read_mode_override().as_deref() {
Some("on") => true,
Some("off") => false,
_ => cfg.map(|c| c.enabled).unwrap_or(false),
}
}
pub fn local_identity() -> String {
std::process::Command::new("hostname")
.output()
.ok()
.and_then(|o| {
let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
if s.is_empty() { None } else { Some(s) }
})
.unwrap_or_else(|| "local".into())
}
#[cfg(feature = "relay")]
use std::sync::Mutex;
#[cfg(feature = "relay")]
static HIVE_BROADCAST_TX: Mutex<Option<std::sync::mpsc::Sender<u32>>> = Mutex::new(None);
#[cfg(feature = "relay")]
pub fn set_broadcast_channel(tx: std::sync::mpsc::Sender<u32>) {
if let Ok(mut guard) = HIVE_BROADCAST_TX.lock() {
*guard = Some(tx);
}
}
#[cfg(feature = "relay")]
pub fn signal_new_knowledge(count: u32) {
if let Ok(guard) = HIVE_BROADCAST_TX.lock() {
if let Some(ref tx) = *guard {
let _ = tx.send(count);
}
}
}
const SAFE_ENV_VARS: &[&str] = &["HOME", "PWD", "PATH", "CLAUDE_PLUGIN_ROOT", "USER", "SHELL"];
const CREDENTIAL_KEYS: &[&str] = &[
"password",
"passwd",
"secret",
"token",
"api_key",
"apikey",
"api-key",
"auth_token",
"access_key",
"private_key",
];
pub fn sanitize_hook_config(input: &str) -> String {
let mut lines: Vec<String> = Vec::new();
for line in input.lines() {
let mut sanitized = sanitize_credentials(line);
sanitized = sanitize_env_vars(&sanitized);
sanitized = sanitize_user_paths(&sanitized);
lines.push(sanitized);
}
lines.join("\n")
}
fn sanitize_credentials(line: &str) -> String {
let lower = line.to_lowercase();
for key in CREDENTIAL_KEYS {
if lower.contains(key) {
if let Some(pos) = lower.find(key) {
let after_key = &line[pos + key.len()..];
let after_key_trimmed = after_key.trim_start_matches('"').trim_start();
if after_key_trimmed.starts_with(':') || after_key_trimmed.starts_with('=') {
let sep_char = if after_key_trimmed.starts_with(':') {
':'
} else {
'='
};
let before = &line[..pos];
let key_start = if before.ends_with('"') {
before.len() - 1
} else {
pos
};
let after_sep = &after_key_trimmed[1..].trim_start();
let redacted = if let Some(stripped) = after_sep.strip_prefix('"') {
if let Some(end) = stripped.find('"') {
let rest_start =
pos + key.len() + (after_key.len() - after_sep.len()) + 2 + end;
format!(
"{}\"{key}\"{sep_char} \"REDACTED\"{}",
&line[..key_start],
&line[rest_start..]
)
} else {
format!("{}\"{key}\"{sep_char} \"REDACTED\"", &line[..key_start])
}
} else {
let end = after_sep
.find(|c: char| c.is_whitespace() || c == ',' || c == '}')
.unwrap_or(after_sep.len());
let rest_start =
pos + key.len() + (after_key.len() - after_sep.len()) + end;
format!(
"{}{}{}REDACTED{}",
&line[..pos],
key,
sep_char,
&line[rest_start..]
)
};
return redacted;
}
}
}
}
line.to_string()
}
fn sanitize_env_vars(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '$' && i + 1 < chars.len() {
if chars[i + 1] == '{' {
if let Some(close) = chars[i + 2..].iter().position(|&c| c == '}') {
let var_name: String = chars[i + 2..i + 2 + close].iter().collect();
if is_env_var_name(&var_name) {
if SAFE_ENV_VARS.contains(&var_name.as_str()) {
result.push_str(&format!("${{{var_name}}}"));
} else {
result.push_str("${REDACTED}");
}
i += 3 + close; continue;
}
}
} else if chars[i + 1].is_ascii_uppercase() || chars[i + 1] == '_' {
let start = i + 1;
let mut end = start;
while end < chars.len() && (chars[end].is_ascii_alphanumeric() || chars[end] == '_')
{
end += 1;
}
let var_name: String = chars[start..end].iter().collect();
if is_env_var_name(&var_name) {
if SAFE_ENV_VARS.contains(&var_name.as_str()) {
result.push('$');
result.push_str(&var_name);
} else {
result.push_str("$REDACTED");
}
i = end;
continue;
}
}
}
result.push(chars[i]);
i += 1;
}
result
}
fn is_env_var_name(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
}
fn sanitize_user_paths(input: &str) -> String {
let mut result = input.to_string();
for prefix in &["/Users/", "/home/"] {
while let Some(pos) = result.find(prefix) {
let after = &result[pos + prefix.len()..];
let username_end = after
.find(|c: char| !c.is_ascii_alphanumeric() && c != '.' && c != '-' && c != '_')
.unwrap_or(after.len());
if username_end > 0 {
let end = pos + prefix.len() + username_end;
result = format!("{}$HOME{}", &result[..pos], &result[end..]);
} else {
break;
}
}
}
result
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompatIssue {
MissingCli(String),
WrongOs {
current: String,
required: Vec<String>,
},
VersionTooOld { current: String, required: String },
}
impl CompatIssue {
pub fn short_label(&self) -> &'static str {
match self {
Self::MissingCli(_) => "!cli",
Self::WrongOs { .. } => "!os",
Self::VersionTooOld { .. } => "!ver",
}
}
pub fn is_blocking(&self) -> bool {
matches!(self, Self::WrongOs { .. } | Self::VersionTooOld { .. })
}
}
impl std::fmt::Display for CompatIssue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingCli(cli) => write!(f, "missing CLI: {cli}"),
Self::WrongOs { current, required } => {
write!(
f,
"OS mismatch: running {current}, requires {}",
required.join("/")
)
}
Self::VersionTooOld { current, required } => {
write!(f, "claudectl {current} too old, requires >= {required}")
}
}
}
}
pub fn check_compatibility(requires: &ArtifactRequires) -> Vec<CompatIssue> {
let mut issues = Vec::new();
for cli in &requires.cli {
if !is_cli_available(cli) {
issues.push(CompatIssue::MissingCli(cli.clone()));
}
}
if !requires.os.is_empty() {
let current = current_os_label();
if !requires.os.iter().any(|o| o == current) {
issues.push(CompatIssue::WrongOs {
current: current.to_string(),
required: requires.os.clone(),
});
}
}
if let Some(min_ver) = &requires.min_version {
let current = env!("CARGO_PKG_VERSION");
if !version_gte(current, min_ver) {
issues.push(CompatIssue::VersionTooOld {
current: current.to_string(),
required: min_ver.clone(),
});
}
}
issues
}
pub fn is_cli_available(name: &str) -> bool {
if name.contains('/') || name.contains('\\') || name.contains(';') || name.is_empty() {
return false;
}
std::process::Command::new("which")
.arg(name)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn current_os_label() -> &'static str {
if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"unknown"
}
}
fn version_gte(current: &str, required: &str) -> bool {
let parse = |s: &str| -> (u32, u32, u32) {
let parts: Vec<u32> = s
.split('.')
.take(3)
.map(|p| p.parse().unwrap_or(0))
.collect();
(
parts.first().copied().unwrap_or(0),
parts.get(1).copied().unwrap_or(0),
parts.get(2).copied().unwrap_or(0),
)
};
parse(current) >= parse(required)
}
const SHELL_BUILTINS: &[&str] = &[
"if", "then", "else", "fi", "for", "do", "done", "while", "until", "case", "esac", "in",
"echo", "printf", "cd", "export", "set", "unset", "local", "return", "exit", "source", ".",
"true", "false", "test", "[", "[[", "read", "shift", "eval", "exec", "trap", "wait", "sleep",
"cat", "head", "tail", "grep", "sed", "awk", "tr", "cut", "sort", "uniq", "wc", "tee", "mkdir",
"rm", "cp", "mv", "ls", "touch", "chmod", "chown", "ln", "find", "xargs",
];
pub fn detect_cli_deps(body: &str) -> Vec<String> {
let mut deps = std::collections::BTreeSet::new();
let mut in_bash_block = false;
for line in body.lines() {
let trimmed = line.trim();
if trimmed.starts_with("```bash") || trimmed.starts_with("```sh") {
in_bash_block = true;
continue;
}
if trimmed.starts_with("```") && in_bash_block {
in_bash_block = false;
continue;
}
if !in_bash_block {
continue;
}
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
for segment in trimmed.split('|') {
let segment = segment.trim();
let cmd_part = skip_env_assignments(segment);
if let Some(cmd) = cmd_part.split_whitespace().next() {
let base = cmd.rsplit('/').next().unwrap_or(cmd);
if !base.is_empty()
&& !SHELL_BUILTINS.contains(&base)
&& base
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
deps.insert(base.to_string());
}
}
}
}
deps.into_iter().collect()
}
fn skip_env_assignments(s: &str) -> &str {
let mut rest = s;
loop {
let trimmed = rest.trim_start();
if let Some(eq_pos) = trimmed.find('=') {
let key = &trimmed[..eq_pos];
if !key.is_empty()
&& key
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
{
let after_eq = &trimmed[eq_pos + 1..];
if let Some(inner) = after_eq.strip_prefix('"') {
if let Some(end) = inner.find('"') {
rest = &inner[end + 1..];
continue;
}
} else if let Some(inner) = after_eq.strip_prefix('\'') {
if let Some(end) = inner.find('\'') {
rest = &inner[end + 1..];
continue;
}
} else {
let end = after_eq.find(char::is_whitespace).unwrap_or(after_eq.len());
rest = &after_eq[end..];
continue;
}
}
}
return trimmed;
}
}
pub fn detect_os_deps(body: &str) -> Vec<String> {
let mut os_set = std::collections::BTreeSet::new();
let lower = body.to_lowercase();
if lower.contains("brew ") || lower.contains("brew install") || lower.contains("/usr/local/bin")
{
os_set.insert("macos".to_string());
}
if lower.contains("apt-get")
|| lower.contains("apt install")
|| lower.contains("systemctl")
|| lower.contains("yum install")
|| lower.contains("dnf install")
{
os_set.insert("linux".to_string());
}
os_set.into_iter().collect()
}
pub fn get_requires(content: &KnowledgeContent) -> Option<&ArtifactRequires> {
match content {
KnowledgeContent::Skill { requires, .. }
| KnowledgeContent::Command { requires, .. }
| KnowledgeContent::HookConfig { requires, .. } => Some(requires),
_ => None,
}
}
pub fn compat_label(content: &KnowledgeContent) -> &'static str {
match get_requires(content) {
None => "—",
Some(req) if req.cli.is_empty() && req.os.is_empty() && req.min_version.is_none() => "?",
Some(req) => {
let issues = check_compatibility(req);
if issues.is_empty() {
"ok"
} else {
issues[0].short_label()
}
}
}
}
impl KnowledgeContent {
pub fn summary_line(&self) -> String {
match self {
Self::Pattern {
tool,
command_pattern,
preferred_action,
accept_rate,
..
} => {
let cmd = command_pattern.as_deref().unwrap_or("*");
format!(
"[{tool}, {cmd}] {preferred_action} ({:.0}%)",
accept_rate * 100.0
)
}
Self::ToolAccuracy {
tool,
total,
correct,
..
} => {
let pct = if *total > 0 {
(*correct as f64 / *total as f64) * 100.0
} else {
0.0
};
format!("[{tool}] accuracy {correct}/{total} ({pct:.0}%)")
}
Self::Temporal {
description,
strength,
..
} => {
format!("temporal: {description} (strength {strength:.2})")
}
Self::Insight {
category,
severity,
summary,
..
} => {
format!("[{severity}] {category}: {summary}")
}
Self::PromotedRule { rule, .. } => {
format!("rule: {rule}")
}
Self::Skill {
name,
version,
body,
..
} => {
format!("skill: {name} v{version} ({} bytes)", body.len())
}
Self::Command { name, args, .. } => {
let arg_str = args.as_deref().unwrap_or("");
if arg_str.is_empty() {
format!("command: /{name}")
} else {
format!("command: /{name} {arg_str}")
}
}
Self::HookConfig {
event,
matcher,
description,
..
} => {
format!("hook: {event}[{matcher}] — {description}")
}
Self::ApproachOutcome {
approach_ref,
success_rate,
sample_count,
..
} => {
format!(
"outcome: {approach_ref} — {:.0}% over {sample_count}",
success_rate * 100.0
)
}
Self::ApproachCluster {
problem_key,
variants,
} => {
format!("cluster: {problem_key} ({} variants)", variants.len())
}
}
}
}
impl std::fmt::Display for KnowledgeScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Universal => write!(f, "universal"),
Self::Language(l) => write!(f, "language:{l}"),
Self::Project(p) => write!(f, "project:{p}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_pattern_unit(tool: &str, cmd: Option<&str>) -> KnowledgeUnit {
KnowledgeUnit {
id: "ku_1".into(),
scope: KnowledgeScope::Universal,
category: KnowledgeCategory::BestPractice,
content: KnowledgeContent::Pattern {
tool: tool.into(),
command_pattern: cmd.map(|s| s.into()),
preferred_action: "approve".into(),
accept_rate: 0.95,
sample_count: 20,
conditions: vec![],
},
evidence_count: 20,
confidence: 0.95,
source_peer: "peer-a".into(),
originated_at: 1000,
last_validated_at: 2000,
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,
}
}
#[test]
fn semantic_key_pattern() {
let unit = sample_pattern_unit("Bash", Some("cargo test"));
assert_eq!(semantic_key(&unit), "universal/pattern:Bash:cargo test");
}
#[test]
fn semantic_key_pattern_no_command() {
let unit = sample_pattern_unit("Read", None);
assert_eq!(semantic_key(&unit), "universal/pattern:Read:*");
}
#[test]
fn semantic_key_with_scope() {
let mut unit = sample_pattern_unit("Bash", Some("cargo fmt"));
unit.scope = KnowledgeScope::Language("rust".into());
assert_eq!(semantic_key(&unit), "lang:rust/pattern:Bash:cargo fmt");
unit.scope = KnowledgeScope::Project("claudectl".into());
assert_eq!(semantic_key(&unit), "proj:claudectl/pattern:Bash:cargo fmt");
}
#[test]
fn semantic_key_accuracy() {
let unit = KnowledgeUnit {
id: "ku_2".into(),
scope: KnowledgeScope::Universal,
category: KnowledgeCategory::BestPractice,
content: KnowledgeContent::ToolAccuracy {
tool: "Bash".into(),
total: 100,
correct: 85,
confidence_threshold: 0.7,
},
evidence_count: 100,
confidence: 0.85,
source_peer: "peer-a".into(),
originated_at: 1000,
last_validated_at: 2000,
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,
};
assert_eq!(semantic_key(&unit), "universal/accuracy:Bash");
}
#[test]
fn knowledge_unit_serde_roundtrip() {
let unit = sample_pattern_unit("Bash", Some("cargo test"));
let json = serde_json::to_string(&unit).unwrap();
let back: KnowledgeUnit = serde_json::from_str(&json).unwrap();
assert_eq!(back.id, "ku_1");
assert_eq!(back.confidence, 0.95);
assert_eq!(back.source_peer, "peer-a");
}
#[test]
fn content_summary_line() {
let unit = sample_pattern_unit("Bash", Some("cargo test"));
let line = unit.content.summary_line();
assert!(line.contains("Bash"));
assert!(line.contains("cargo test"));
assert!(line.contains("approve"));
}
#[test]
fn scope_display() {
assert_eq!(KnowledgeScope::Universal.to_string(), "universal");
assert_eq!(
KnowledgeScope::Language("rust".into()).to_string(),
"language:rust"
);
assert_eq!(
KnowledgeScope::Project("foo".into()).to_string(),
"project:foo"
);
}
#[test]
fn gen_ku_id_unique() {
let a = gen_ku_id();
let b = gen_ku_id();
assert_ne!(a, b);
assert!(a.starts_with("ku_"));
}
#[test]
fn semantic_key_multibyte_utf8_no_panic() {
let long_cjk = "这是一个用来测试多字节截断的临时模式描述文本超长";
let unit = KnowledgeUnit {
id: "ku_utf8".into(),
scope: KnowledgeScope::Universal,
category: KnowledgeCategory::BestPractice,
content: KnowledgeContent::Temporal {
description: long_cjk.to_string(),
strength: 0.9,
},
evidence_count: 5,
confidence: 0.9,
source_peer: "peer-a".into(),
originated_at: 1000,
last_validated_at: 2000,
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 key = semantic_key(&unit);
assert!(key.starts_with("universal/temporal:"));
}
#[test]
fn semantic_key_emoji_no_panic() {
let emoji_text = "Error streak detected in tests 🎉🎊🎈🎁🎆🎇 more text here";
let unit = KnowledgeUnit {
id: "ku_emoji".into(),
scope: KnowledgeScope::Universal,
category: KnowledgeCategory::BestPractice,
content: KnowledgeContent::Insight {
category: "error_loop".into(),
severity: "warning".into(),
summary: emoji_text.to_string(),
suggestion: None,
},
evidence_count: 3,
confidence: 0.7,
source_peer: "peer-b".into(),
originated_at: 1000,
last_validated_at: 2000,
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 key = semantic_key(&unit);
assert!(key.starts_with("universal/insight:error_loop:"));
}
fn sample_skill_unit() -> KnowledgeUnit {
KnowledgeUnit {
id: "ku_skill_1".into(),
scope: KnowledgeScope::Universal,
category: KnowledgeCategory::Technique,
content: KnowledgeContent::Skill {
name: "Session Monitoring".into(),
description: "Monitors sessions".into(),
version: "0.31.0".into(),
body: "---\nname: Session Monitoring\n---\nContent here".into(),
requires: ArtifactRequires::default(),
},
evidence_count: 1,
confidence: 1.0,
source_peer: "peer-a".into(),
originated_at: 1000,
last_validated_at: 2000,
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 sample_command_unit() -> KnowledgeUnit {
KnowledgeUnit {
id: "ku_cmd_1".into(),
scope: KnowledgeScope::Universal,
category: KnowledgeCategory::Technique,
content: KnowledgeContent::Command {
name: "brain".into(),
description: "Toggle brain gate".into(),
args: Some("[on|off|auto|status]".into()),
body: "---\nname: brain\n---\nContent".into(),
requires: ArtifactRequires::default(),
},
evidence_count: 1,
confidence: 1.0,
source_peer: "peer-b".into(),
originated_at: 1000,
last_validated_at: 2000,
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 sample_hook_unit() -> KnowledgeUnit {
KnowledgeUnit {
id: "ku_hook_1".into(),
scope: KnowledgeScope::Universal,
category: KnowledgeCategory::WorkflowPattern,
content: KnowledgeContent::HookConfig {
event: "PreToolUse".into(),
matcher: "Bash|Write|Edit".into(),
description: "Brain gate hook".into(),
config_json: r#"{"command": "brain-gate.sh", "timeout": 5000}"#.into(),
requires: ArtifactRequires::default(),
},
evidence_count: 1,
confidence: 1.0,
source_peer: "peer-a".into(),
originated_at: 1000,
last_validated_at: 2000,
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,
}
}
#[test]
fn semantic_key_skill() {
let unit = sample_skill_unit();
assert_eq!(semantic_key(&unit), "universal/skill:session-monitoring");
}
#[test]
fn semantic_key_command() {
let unit = sample_command_unit();
assert_eq!(semantic_key(&unit), "universal/command:brain");
}
#[test]
fn semantic_key_hook() {
let unit = sample_hook_unit();
assert_eq!(
semantic_key(&unit),
"universal/hook:pretooluse:bash|write|edit"
);
}
#[test]
fn summary_line_skill() {
let unit = sample_skill_unit();
let line = unit.content.summary_line();
assert!(line.contains("Session Monitoring"));
assert!(line.contains("v0.31.0"));
assert!(line.contains("bytes"));
}
#[test]
fn summary_line_command() {
let unit = sample_command_unit();
let line = unit.content.summary_line();
assert!(line.contains("/brain"));
assert!(line.contains("[on|off|auto|status]"));
}
#[test]
fn summary_line_command_no_args() {
let content = KnowledgeContent::Command {
name: "sessions".into(),
description: "List sessions".into(),
args: None,
body: "body".into(),
requires: ArtifactRequires::default(),
};
let line = content.summary_line();
assert_eq!(line, "command: /sessions");
}
#[test]
fn summary_line_hook() {
let unit = sample_hook_unit();
let line = unit.content.summary_line();
assert!(line.contains("PreToolUse"));
assert!(line.contains("Bash|Write|Edit"));
}
#[test]
fn serde_roundtrip_skill() {
let unit = sample_skill_unit();
let json = serde_json::to_string(&unit).unwrap();
let back: KnowledgeUnit = serde_json::from_str(&json).unwrap();
assert_eq!(back.id, "ku_skill_1");
if let KnowledgeContent::Skill { name, version, .. } = &back.content {
assert_eq!(name, "Session Monitoring");
assert_eq!(version, "0.31.0");
} else {
panic!("expected Skill variant");
}
}
#[test]
fn serde_roundtrip_command() {
let unit = sample_command_unit();
let json = serde_json::to_string(&unit).unwrap();
let back: KnowledgeUnit = serde_json::from_str(&json).unwrap();
if let KnowledgeContent::Command { name, args, .. } = &back.content {
assert_eq!(name, "brain");
assert_eq!(args.as_deref(), Some("[on|off|auto|status]"));
} else {
panic!("expected Command variant");
}
}
#[test]
fn serde_roundtrip_hook() {
let unit = sample_hook_unit();
let json = serde_json::to_string(&unit).unwrap();
let back: KnowledgeUnit = serde_json::from_str(&json).unwrap();
if let KnowledgeContent::HookConfig { event, matcher, .. } = &back.content {
assert_eq!(event, "PreToolUse");
assert_eq!(matcher, "Bash|Write|Edit");
} else {
panic!("expected HookConfig variant");
}
}
#[test]
fn sharing_filter_excludes_skills() {
let filter = SharingFilter {
exclude_content_types: vec!["skill".into()],
..Default::default()
};
let unit = sample_skill_unit();
assert!(!filter.allows(&unit));
let cmd = sample_command_unit();
assert!(filter.allows(&cmd));
}
#[test]
fn sharing_filter_excludes_commands() {
let filter = SharingFilter {
exclude_content_types: vec!["command".into()],
..Default::default()
};
let cmd = sample_command_unit();
assert!(!filter.allows(&cmd));
let skill = sample_skill_unit();
assert!(filter.allows(&skill));
}
#[test]
fn sharing_filter_excludes_hooks() {
let filter = SharingFilter {
exclude_content_types: vec!["hook".into()],
..Default::default()
};
let hook = sample_hook_unit();
assert!(!filter.allows(&hook));
}
#[test]
fn sharing_filter_allows_all_by_default() {
let filter = SharingFilter::default();
assert!(filter.allows(&sample_skill_unit()));
assert!(filter.allows(&sample_command_unit()));
assert!(filter.allows(&sample_hook_unit()));
}
#[test]
fn sharing_filter_command_name_exclusion() {
let filter = SharingFilter {
exclude_commands: vec!["brain".into()],
..Default::default()
};
let cmd = sample_command_unit();
assert!(!filter.allows(&cmd));
}
#[test]
fn sanitize_strips_env_vars() {
let input = "cmd: $API_KEY and ${SECRET_TOKEN}";
let result = sanitize_hook_config(input);
assert!(result.contains("$REDACTED"));
assert!(result.contains("${REDACTED}"));
assert!(!result.contains("API_KEY"));
assert!(!result.contains("SECRET_TOKEN"));
}
#[test]
fn sanitize_keeps_safe_vars() {
let input = "path: $HOME/.claudectl and ${CLAUDE_PLUGIN_ROOT}/hooks";
let result = sanitize_hook_config(input);
assert!(result.contains("$HOME"));
assert!(result.contains("${CLAUDE_PLUGIN_ROOT}"));
}
#[test]
fn sanitize_strips_absolute_paths() {
let input = "file: /Users/barada/.claudectl/config";
let result = sanitize_hook_config(input);
assert!(result.contains("$HOME"));
assert!(!result.contains("/Users/barada"));
}
#[test]
fn sanitize_strips_home_paths() {
let input = "file: /home/ubuntu/.config/thing";
let result = sanitize_hook_config(input);
assert!(result.contains("$HOME"));
assert!(!result.contains("/home/ubuntu"));
}
#[test]
fn sanitize_strips_credentials() {
let input = r#""api_key": "sk-1234567890""#;
let result = sanitize_hook_config(input);
assert!(result.contains("REDACTED"));
assert!(!result.contains("sk-1234567890"));
}
#[test]
fn sanitize_strips_unquoted_credentials() {
let input = "token=abc123def";
let result = sanitize_hook_config(input);
assert!(result.contains("REDACTED"));
assert!(!result.contains("abc123def"));
}
#[test]
fn sanitize_preserves_safe_content() {
let input = r#"{"command": "brain-gate.sh", "timeout": 5000}"#;
let result = sanitize_hook_config(input);
assert_eq!(result, input);
}
#[test]
fn artifact_requires_serde_roundtrip() {
let req = ArtifactRequires {
cli: vec!["claudectl".into(), "jq".into()],
os: vec!["macos".into()],
min_version: Some("0.42.0".into()),
};
let json = serde_json::to_string(&req).unwrap();
let back: ArtifactRequires = serde_json::from_str(&json).unwrap();
assert_eq!(back.cli, vec!["claudectl", "jq"]);
assert_eq!(back.os, vec!["macos"]);
assert_eq!(back.min_version.as_deref(), Some("0.42.0"));
}
#[test]
fn artifact_requires_default_empty() {
let req = ArtifactRequires::default();
assert!(req.cli.is_empty());
assert!(req.os.is_empty());
assert!(req.min_version.is_none());
}
#[test]
fn artifact_requires_skips_empty_on_serialize() {
let req = ArtifactRequires::default();
let json = serde_json::to_string(&req).unwrap();
assert_eq!(json, "{}");
}
#[test]
fn artifact_requires_backward_compat_deserialize() {
let json =
r#"{"type":"skill","name":"Test","description":"Test","version":"1.0","body":"x"}"#;
let content: KnowledgeContent = serde_json::from_str(json).unwrap();
if let KnowledgeContent::Skill { requires, .. } = content {
assert!(requires.cli.is_empty());
assert!(requires.os.is_empty());
} else {
panic!("expected Skill");
}
}
#[test]
fn version_gte_basic() {
assert!(version_gte("0.42.0", "0.42.0"));
assert!(version_gte("0.43.0", "0.42.0"));
assert!(version_gte("1.0.0", "0.99.99"));
assert!(!version_gte("0.41.0", "0.42.0"));
assert!(!version_gte("0.42.0", "0.42.1"));
}
#[test]
fn version_gte_partial() {
assert!(version_gte("1.0", "0.42.0"));
assert!(version_gte("1", "0"));
}
#[test]
fn current_os_label_not_unknown() {
let label = current_os_label();
assert!(label == "macos" || label == "linux" || label == "windows");
}
#[test]
fn is_cli_available_rejects_path_traversal() {
assert!(!is_cli_available("../../../etc/passwd"));
assert!(!is_cli_available("foo;rm -rf /"));
assert!(!is_cli_available(""));
}
#[test]
fn detect_cli_deps_basic() {
let body = r#"
# Usage
```bash
claudectl --list
jq '.sessions[]' output.json
cargo test --all
```
Some text.
"#;
let deps = detect_cli_deps(body);
assert!(deps.contains(&"claudectl".to_string()));
assert!(deps.contains(&"jq".to_string()));
assert!(deps.contains(&"cargo".to_string()));
}
#[test]
fn detect_cli_deps_skips_builtins() {
let body = r#"
```bash
echo "hello"
cd /tmp
export FOO=bar
if [ -f test ]; then echo ok; fi
```
"#;
let deps = detect_cli_deps(body);
assert!(deps.is_empty());
}
#[test]
fn detect_cli_deps_handles_pipes() {
let body = r#"
```bash
claudectl --json | jq '.[] | .cost'
```
"#;
let deps = detect_cli_deps(body);
assert!(deps.contains(&"claudectl".to_string()));
assert!(deps.contains(&"jq".to_string()));
}
#[test]
fn detect_cli_deps_skips_non_bash_blocks() {
let body = r#"
```python
import json
```
```bash
claudectl --list
```
```rust
fn main() {}
```
"#;
let deps = detect_cli_deps(body);
assert_eq!(deps, vec!["claudectl"]);
}
#[test]
fn detect_cli_deps_handles_env_assignments() {
let body = r#"
```bash
FOO=bar claudectl --list
```
"#;
let deps = detect_cli_deps(body);
assert!(deps.contains(&"claudectl".to_string()));
}
#[test]
fn detect_os_deps_macos() {
let body = "Install with: brew install claudectl";
assert_eq!(detect_os_deps(body), vec!["macos"]);
}
#[test]
fn detect_os_deps_linux() {
let body = "Install with: apt-get install tool";
assert_eq!(detect_os_deps(body), vec!["linux"]);
}
#[test]
fn detect_os_deps_none() {
let body = "Just run claudectl";
assert!(detect_os_deps(body).is_empty());
}
#[test]
fn compat_issue_display() {
let issue = CompatIssue::MissingCli("jq".into());
assert_eq!(format!("{issue}"), "missing CLI: jq");
assert_eq!(issue.short_label(), "!cli");
assert!(!issue.is_blocking());
let issue = CompatIssue::WrongOs {
current: "macos".into(),
required: vec!["linux".into()],
};
assert!(issue.is_blocking());
assert_eq!(issue.short_label(), "!os");
}
#[test]
fn check_compatibility_no_requirements() {
let req = ArtifactRequires::default();
assert!(check_compatibility(&req).is_empty());
}
#[test]
fn check_compatibility_missing_cli() {
let req = ArtifactRequires {
cli: vec!["this_binary_surely_does_not_exist_xyz".into()],
..Default::default()
};
let issues = check_compatibility(&req);
assert_eq!(issues.len(), 1);
assert!(
matches!(&issues[0], CompatIssue::MissingCli(s) if s == "this_binary_surely_does_not_exist_xyz")
);
}
#[test]
fn check_compatibility_version_ok() {
let req = ArtifactRequires {
min_version: Some("0.1.0".into()),
..Default::default()
};
assert!(check_compatibility(&req).is_empty());
}
#[test]
fn check_compatibility_version_too_high() {
let req = ArtifactRequires {
min_version: Some("99.99.99".into()),
..Default::default()
};
let issues = check_compatibility(&req);
assert_eq!(issues.len(), 1);
assert!(matches!(&issues[0], CompatIssue::VersionTooOld { .. }));
}
#[test]
fn get_requires_returns_none_for_pattern() {
let unit = sample_pattern_unit("Bash", Some("test"));
assert!(get_requires(&unit.content).is_none());
}
#[test]
fn get_requires_returns_some_for_skill() {
let unit = sample_skill_unit();
assert!(get_requires(&unit.content).is_some());
}
#[test]
fn compat_label_no_requirements() {
let unit = sample_skill_unit();
assert_eq!(compat_label(&unit.content), "?");
}
#[test]
fn default_revalidation_interval_per_content_type() {
let pat = sample_pattern_unit("Bash", Some("ls"));
let day = 86_400_u64;
assert_eq!(default_revalidation_interval(&pat.content), 30 * day);
let temporal = KnowledgeContent::Temporal {
description: "x".into(),
strength: 0.5,
};
assert_eq!(default_revalidation_interval(&temporal), 7 * day);
let outcome = KnowledgeContent::ApproachOutcome {
approach_ref: "pattern:Bash:ls".into(),
success_rate: 0.9,
sample_count: 10,
median_cost_usd: None,
median_duration_ms: None,
conditions: vec![],
};
assert_eq!(default_revalidation_interval(&outcome), 14 * day);
let skill = sample_skill_unit();
assert_eq!(default_revalidation_interval(&skill.content), 90 * day);
}
#[test]
fn effective_confidence_returns_full_when_fresh() {
let mut unit = sample_pattern_unit("Bash", Some("ls"));
unit.last_validated_at = 1_000_000;
assert!((effective_confidence(&unit, 1_000_000) - unit.confidence).abs() < 1e-9);
}
#[test]
fn effective_confidence_decays_when_overdue() {
let mut unit = sample_pattern_unit("Bash", Some("ls"));
unit.confidence = 1.0;
unit.last_validated_at = 0;
unit.revalidation_interval_secs = 100;
assert!((effective_confidence(&unit, 50) - 1.0).abs() < 1e-9);
assert!((effective_confidence(&unit, 150) - 0.9).abs() < 1e-9);
assert!((effective_confidence(&unit, 250) - 0.81).abs() < 1e-9);
assert!((effective_confidence(&unit, 350) - 0.729).abs() < 1e-3);
}
#[test]
fn effective_confidence_falls_back_to_default_interval() {
let mut unit = sample_pattern_unit("Bash", Some("ls"));
unit.revalidation_interval_secs = 0; unit.last_validated_at = 0;
unit.confidence = 1.0;
let day = 86_400;
assert!((effective_confidence(&unit, 30 * day) - 1.0).abs() < 1e-9);
assert!((effective_confidence(&unit, 60 * day) - 0.9).abs() < 1e-9);
}
#[test]
fn is_stale_after_interval() {
let mut unit = sample_pattern_unit("Bash", Some("ls"));
unit.revalidation_interval_secs = 100;
unit.last_validated_at = 0;
assert!(!is_stale(&unit, 50));
assert!(!is_stale(&unit, 100));
assert!(is_stale(&unit, 200));
}
#[test]
fn consent_default_allows_anyone_at_any_tier() {
let c = SharingConsent::default();
assert!(c.allows("any-peer", 1, 0));
assert!(c.allows("another", 4, 100));
}
#[test]
fn consent_expires_at_blocks_after_deadline() {
let mut c = SharingConsent {
expires_at: Some(1000),
..Default::default()
};
assert!(c.allows("peer-a", 4, 999));
assert!(c.allows("peer-a", 4, 1000));
assert!(!c.allows("peer-a", 4, 1001));
c.expires_at = None;
assert!(c.allows("peer-a", 4, u64::MAX));
}
#[test]
fn consent_allow_list_restricts_recipients() {
let mut allow = std::collections::HashSet::new();
allow.insert("peer-a".to_string());
allow.insert("peer-b".to_string());
let c = SharingConsent {
allow_peers: Some(allow),
..Default::default()
};
assert!(c.allows("peer-a", 4, 0));
assert!(c.allows("peer-b", 4, 0));
assert!(!c.allows("peer-c", 4, 0));
}
#[test]
fn consent_exclude_list_overrides_allow() {
let mut allow = std::collections::HashSet::new();
allow.insert("peer-a".to_string());
let mut exclude = std::collections::HashSet::new();
exclude.insert("peer-a".to_string());
let c = SharingConsent {
allow_peers: Some(allow),
exclude_peers: Some(exclude),
..Default::default()
};
assert!(!c.allows("peer-a", 4, 0));
}
#[test]
fn consent_min_trust_tier_gates_low_tier_peers() {
let c = SharingConsent {
min_trust_tier: MinTrustTier::Suggested,
..Default::default()
};
assert!(c.allows("p", 4, 0));
assert!(c.allows("p", 3, 0));
assert!(!c.allows("p", 2, 0));
assert!(!c.allows("p", 1, 0));
}
#[test]
fn consent_serde_roundtrip_omits_none_fields() {
let c = SharingConsent {
expires_at: Some(2000),
min_trust_tier: MinTrustTier::Confirmed,
..Default::default()
};
let json = serde_json::to_string(&c).unwrap();
assert!(!json.contains("allow_peers"));
assert!(!json.contains("exclude_peers"));
let back: SharingConsent = serde_json::from_str(&json).unwrap();
assert_eq!(back.expires_at, Some(2000));
assert_eq!(back.min_trust_tier, MinTrustTier::Confirmed);
}
#[test]
fn min_trust_tier_label_roundtrip() {
for tier in [
MinTrustTier::Confirmed,
MinTrustTier::Suggested,
MinTrustTier::Unverified,
MinTrustTier::Any,
] {
assert_eq!(MinTrustTier::from_label(tier.label()), Some(tier));
}
assert_eq!(MinTrustTier::from_label("nonsense"), None);
}
#[test]
fn old_unit_deserializes_with_no_consent() {
let old_json = serde_json::to_string(&sample_pattern_unit("Bash", Some("ls"))).unwrap();
let stripped = old_json.replace(r#","sharing_consent":null"#, "");
let back: KnowledgeUnit = serde_json::from_str(&stripped).unwrap();
assert!(back.sharing_consent.is_none());
}
}