use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PersonaSnapshot {
pub identity: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub salient_traits: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style_tone: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillSnapshot {
pub id: String,
pub name: String,
pub instruction_fragment: String,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default = "default_priority")]
pub priority: u8,
#[serde(default)]
pub tools: Vec<String>,
}
fn default_priority() -> u8 {
5
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PolicySnapshot {
#[serde(default)]
pub allowed_tools: Vec<String>,
#[serde(default)]
pub allowed_models: Vec<String>,
#[serde(default)]
pub auto_approve_unrestricted: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens_per_call: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerSnapshot {
pub name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolSetSnapshot {
pub id: String,
#[serde(default)]
pub mcp_servers: Vec<McpServerSnapshot>,
#[serde(default)]
pub tool_names: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConceptProjection {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persona: Option<PersonaSnapshot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<SkillSnapshot>,
#[serde(default)]
pub policy: PolicySnapshot,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub toolsets: Vec<ToolSetSnapshot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_memory: Option<String>,
}
impl ConceptProjection {
pub fn is_empty(&self) -> bool {
self.persona.is_none()
&& self.skills.is_empty()
&& self.toolsets.is_empty()
&& self.project_memory.is_none()
&& self.policy.allowed_tools.is_empty()
&& self.policy.allowed_models.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_projection_is_empty() {
assert!(ConceptProjection::default().is_empty());
}
#[test]
fn skill_round_trip() {
let s = SkillSnapshot {
id: "rag".into(),
name: "RAG".into(),
instruction_fragment: "use the index".into(),
keywords: vec!["search".into()],
priority: 7,
tools: vec!["WebSearch".into()],
};
let j = serde_json::to_string(&s).unwrap();
let back: SkillSnapshot = serde_json::from_str(&j).unwrap();
assert_eq!(back.id, "rag");
assert_eq!(back.priority, 7);
}
}