use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use crate::{Document, InMemoryStore};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CorpusType {
SigilDocs,
Checkpoints,
ResolvedFrictions,
Patterns,
SourceCode,
GeneratedSigil,
}
impl CorpusType {
pub fn namespace(&self) -> &'static str {
match self {
Self::SigilDocs => "sigil_docs",
Self::Checkpoints => "checkpoints",
Self::ResolvedFrictions => "frictions",
Self::Patterns => "patterns",
Self::SourceCode => "source",
Self::GeneratedSigil => "generated",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExperienceCheckpoint {
pub id: String,
pub project: String,
pub phase: ConversionPhase,
pub timestamp: DateTime<Utc>,
pub agent_id: String,
pub model_id: String,
pub duration_secs: u64,
pub lines_converted: u32,
pub sigil_lines_written: u32,
pub ratio: f32,
pub joys: Vec<Joy>,
pub frictions: Vec<Friction>,
pub patterns_discovered: Vec<Pattern>,
pub missing_features: Vec<FeatureGap>,
pub confidence: Evidentiality,
pub would_use_again: bool,
pub notes: Option<String>,
}
impl ExperienceCheckpoint {
pub fn new(project: impl Into<String>, phase: ConversionPhase) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
project: project.into(),
phase,
timestamp: Utc::now(),
agent_id: String::new(),
model_id: String::new(),
duration_secs: 0,
lines_converted: 0,
sigil_lines_written: 0,
ratio: 1.0,
joys: Vec::new(),
frictions: Vec::new(),
patterns_discovered: Vec::new(),
missing_features: Vec::new(),
confidence: Evidentiality::Reported,
would_use_again: true,
notes: None,
}
}
pub fn with_agent(mut self, agent_id: impl Into<String>, model_id: impl Into<String>) -> Self {
self.agent_id = agent_id.into();
self.model_id = model_id.into();
self
}
pub fn add_joy(&mut self, joy: Joy) {
self.joys.push(joy);
}
pub fn add_friction(&mut self, friction: Friction) {
self.frictions.push(friction);
}
pub fn add_pattern(&mut self, pattern: Pattern) {
self.patterns_discovered.push(pattern);
}
pub fn joy_friction_ratio(&self) -> f32 {
if self.frictions.is_empty() {
return f32::INFINITY;
}
self.joys.len() as f32 / self.frictions.len() as f32
}
pub fn to_document(&self) -> Document {
let content = format!(
"Project: {}\nPhase: {:?}\nAgent: {}\nModel: {}\n\
Lines: {} → {} (ratio: {:.2})\n\
Joys: {:?}\nFrictions: {:?}\nPatterns: {:?}\n\
Notes: {}",
self.project,
self.phase,
self.agent_id,
self.model_id,
self.lines_converted,
self.sigil_lines_written,
self.ratio,
self.joys.iter().map(|j| &j.description).collect::<Vec<_>>(),
self.frictions
.iter()
.map(|f| &f.description)
.collect::<Vec<_>>(),
self.patterns_discovered
.iter()
.map(|p| &p.name)
.collect::<Vec<_>>(),
self.notes.as_deref().unwrap_or("None")
);
let mut metadata: HashMap<String, serde_json::Value> = HashMap::new();
metadata.insert(
"project".to_string(),
serde_json::Value::String(self.project.clone()),
);
metadata.insert(
"phase".to_string(),
serde_json::Value::String(format!("{:?}", self.phase)),
);
metadata.insert(
"agent_id".to_string(),
serde_json::Value::String(self.agent_id.clone()),
);
metadata.insert(
"model_id".to_string(),
serde_json::Value::String(self.model_id.clone()),
);
metadata.insert("joy_count".to_string(), serde_json::json!(self.joys.len()));
metadata.insert(
"friction_count".to_string(),
serde_json::json!(self.frictions.len()),
);
Document {
id: self.id.clone(),
content,
metadata,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ConversionPhase {
Analysis,
Design,
Core,
Logic,
Integration,
Polish,
Validation,
}
impl ConversionPhase {
pub fn collaboration_mode(&self) -> CollaborationMode {
match self {
Self::Analysis => CollaborationMode::Solo,
Self::Design => CollaborationMode::Solo,
Self::Core => CollaborationMode::Pair,
Self::Logic => CollaborationMode::Pair,
Self::Integration => CollaborationMode::Solo,
Self::Polish => CollaborationMode::Solo,
Self::Validation => CollaborationMode::Independent,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CollaborationMode {
Solo,
Pair,
Independent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Joy {
pub description: String,
pub category: JoyCategory,
pub intensity: f32,
pub example: Option<String>,
pub reproducible: bool,
}
impl Joy {
pub fn new(description: impl Into<String>, category: JoyCategory) -> Self {
Self {
description: description.into(),
category,
intensity: 0.5,
example: None,
reproducible: true,
}
}
pub fn with_intensity(mut self, intensity: f32) -> Self {
self.intensity = intensity.clamp(0.0, 1.0);
self
}
pub fn with_example(mut self, example: impl Into<String>) -> Self {
self.example = Some(example.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum JoyCategory {
Expressiveness,
Safety,
Clarity,
Power,
Elegance,
Discovery,
Flow,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Friction {
pub description: String,
pub category: FrictionCategory,
pub severity: Severity,
pub workaround: Option<String>,
pub blocking: bool,
pub example: Option<String>,
}
impl Friction {
pub fn new(description: impl Into<String>, category: FrictionCategory) -> Self {
Self {
description: description.into(),
category,
severity: Severity::Moderate,
workaround: None,
blocking: false,
example: None,
}
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn with_workaround(mut self, workaround: impl Into<String>) -> Self {
self.workaround = Some(workaround.into());
self
}
pub fn blocking(mut self) -> Self {
self.blocking = true;
self.severity = Severity::Blocking;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FrictionCategory {
Syntax,
Semantics,
Tooling,
Documentation,
MissingFeature,
Performance,
ErrorMessages,
Interop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Severity {
Minor,
Moderate,
Major,
Blocking,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pattern {
pub name: String,
pub description: String,
pub example: String,
pub frequency: Frequency,
pub should_be_builtin: bool,
}
impl Pattern {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
example: impl Into<String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
example: example.into(),
frequency: Frequency::Sometimes,
should_be_builtin: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Frequency {
Once,
Sometimes,
Often,
Always,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureGap {
pub description: String,
pub use_case: String,
pub workaround: Option<String>,
pub priority: GapPriority,
pub similar_in_other_langs: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum GapPriority {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Evidentiality {
Known,
Uncertain,
Reported,
}
pub struct SigilKnowledgeBase {
stores: HashMap<CorpusType, Arc<InMemoryStore>>,
checkpoints: RwLock<Vec<ExperienceCheckpoint>>,
patterns: RwLock<Vec<Pattern>>,
root_dir: Option<PathBuf>,
}
impl SigilKnowledgeBase {
pub fn new() -> Self {
let mut stores = HashMap::new();
for corpus_type in &[
CorpusType::SigilDocs,
CorpusType::Checkpoints,
CorpusType::ResolvedFrictions,
CorpusType::Patterns,
CorpusType::SourceCode,
CorpusType::GeneratedSigil,
] {
stores.insert(*corpus_type, Arc::new(InMemoryStore::new()));
}
Self {
stores,
checkpoints: RwLock::new(Vec::new()),
patterns: RwLock::new(Vec::new()),
root_dir: None,
}
}
pub fn with_persistence(root_dir: PathBuf) -> std::io::Result<Self> {
std::fs::create_dir_all(&root_dir)?;
let mut kb = Self::new();
kb.root_dir = Some(root_dir);
Ok(kb)
}
pub fn store(&self, corpus_type: CorpusType) -> Option<Arc<InMemoryStore>> {
self.stores.get(&corpus_type).cloned()
}
pub fn add_checkpoint(&self, checkpoint: ExperienceCheckpoint) {
self.checkpoints.write().push(checkpoint.clone());
for pattern in &checkpoint.patterns_discovered {
self.patterns.write().push(pattern.clone());
}
}
pub fn checkpoints(&self) -> Vec<ExperienceCheckpoint> {
self.checkpoints.read().clone()
}
pub fn checkpoints_for_project(&self, project: &str) -> Vec<ExperienceCheckpoint> {
self.checkpoints
.read()
.iter()
.filter(|c| c.project == project)
.cloned()
.collect()
}
pub fn patterns(&self) -> Vec<Pattern> {
self.patterns.read().clone()
}
pub fn generate_report(&self) -> ResearchReport {
let checkpoints = self.checkpoints.read();
let total_joys: usize = checkpoints.iter().map(|c| c.joys.len()).sum();
let total_frictions: usize = checkpoints.iter().map(|c| c.frictions.len()).sum();
let mut joy_by_category: HashMap<JoyCategory, Vec<&Joy>> = HashMap::new();
for cp in checkpoints.iter() {
for joy in &cp.joys {
joy_by_category.entry(joy.category).or_default().push(joy);
}
}
let mut friction_by_category: HashMap<FrictionCategory, Vec<&Friction>> = HashMap::new();
for cp in checkpoints.iter() {
for friction in &cp.frictions {
friction_by_category
.entry(friction.category)
.or_default()
.push(friction);
}
}
let mut friction_counts: Vec<_> = friction_by_category
.iter()
.map(|(cat, frictions)| (*cat, frictions.len()))
.collect();
friction_counts.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
ResearchReport {
checkpoint_count: checkpoints.len(),
total_joys,
total_frictions,
joy_friction_ratio: if total_frictions > 0 {
total_joys as f32 / total_frictions as f32
} else {
f32::INFINITY
},
top_friction_categories: friction_counts
.into_iter()
.take(5)
.map(|(cat, count)| (cat, count))
.collect(),
pattern_count: self.patterns.read().len(),
projects_analyzed: checkpoints
.iter()
.map(|c| c.project.clone())
.collect::<std::collections::HashSet<_>>()
.len(),
}
}
pub fn save(&self) -> std::io::Result<()> {
if let Some(root) = &self.root_dir {
let checkpoints_file = root.join("checkpoints.json");
let checkpoints = self.checkpoints.read();
let content = serde_json::to_string_pretty(&*checkpoints)?;
std::fs::write(checkpoints_file, content)?;
let patterns_file = root.join("patterns.json");
let patterns = self.patterns.read();
let content = serde_json::to_string_pretty(&*patterns)?;
std::fs::write(patterns_file, content)?;
}
Ok(())
}
pub fn load(&self) -> std::io::Result<()> {
if let Some(root) = &self.root_dir {
let checkpoints_file = root.join("checkpoints.json");
if checkpoints_file.exists() {
let content = std::fs::read_to_string(checkpoints_file)?;
let loaded: Vec<ExperienceCheckpoint> = serde_json::from_str(&content)?;
*self.checkpoints.write() = loaded;
}
let patterns_file = root.join("patterns.json");
if patterns_file.exists() {
let content = std::fs::read_to_string(patterns_file)?;
let loaded: Vec<Pattern> = serde_json::from_str(&content)?;
*self.patterns.write() = loaded;
}
}
Ok(())
}
}
impl Default for SigilKnowledgeBase {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ResearchReport {
pub checkpoint_count: usize,
pub total_joys: usize,
pub total_frictions: usize,
pub joy_friction_ratio: f32,
pub top_friction_categories: Vec<(FrictionCategory, usize)>,
pub pattern_count: usize,
pub projects_analyzed: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checkpoint_creation() {
let mut checkpoint = ExperienceCheckpoint::new("infernum", ConversionPhase::Core)
.with_agent("agent-001", "claude-opus-4");
checkpoint.add_joy(
Joy::new("Type inference is excellent", JoyCategory::Safety).with_intensity(0.9),
);
checkpoint.add_friction(
Friction::new("Async syntax is verbose", FrictionCategory::Syntax)
.with_severity(Severity::Minor),
);
assert_eq!(checkpoint.project, "infernum");
assert_eq!(checkpoint.joys.len(), 1);
assert_eq!(checkpoint.frictions.len(), 1);
assert_eq!(checkpoint.joy_friction_ratio(), 1.0);
}
#[test]
fn test_knowledge_base() {
let kb = SigilKnowledgeBase::new();
let checkpoint = ExperienceCheckpoint::new("test-project", ConversionPhase::Analysis);
kb.add_checkpoint(checkpoint);
let checkpoints = kb.checkpoints();
assert_eq!(checkpoints.len(), 1);
let report = kb.generate_report();
assert_eq!(report.checkpoint_count, 1);
}
#[test]
fn test_conversion_phase_collaboration() {
assert_eq!(
ConversionPhase::Validation.collaboration_mode(),
CollaborationMode::Independent
);
assert_eq!(
ConversionPhase::Core.collaboration_mode(),
CollaborationMode::Pair
);
}
}