Skip to main content

mur_common/
knowledge.rs

1//! KnowledgeBase — the shared foundation for patterns and workflows.
2//!
3//! Both `Pattern` and `Workflow` embed `KnowledgeBase` via `#[serde(flatten)]`
4//! so their YAML stays flat (no nested `base:` key).
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::Scope;
10use crate::pattern::{
11    Applies, Content, Evidence, Lifecycle, Links, Tags, Tier, default_confidence,
12    default_importance, default_schema,
13};
14
15/// Maturity level for knowledge items.
16#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
17#[serde(rename_all = "lowercase")]
18pub enum Maturity {
19    #[default]
20    Draft,
21    Emerging,
22    Stable,
23    Canonical,
24}
25
26/// Decay metadata for time-based relevance.
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct DecayMeta {
29    /// Last time this knowledge was actively used/referenced
30    pub last_active: Option<DateTime<Utc>>,
31    /// Override the tier-based half-life (in days)
32    pub half_life_override: Option<u32>,
33}
34
35fn is_zero_u32(v: &u32) -> bool {
36    *v == 0
37}
38
39/// Shared fields for all knowledge items (patterns, workflows).
40///
41/// Embedded via `#[serde(flatten)]` so YAML stays flat.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct KnowledgeBase {
44    #[serde(default = "default_schema")]
45    pub schema: u32,
46
47    /// Unique identifier (kebab-case, e.g. "swift-testing-macro")
48    pub name: String,
49
50    /// Human-readable one-line description
51    pub description: String,
52
53    /// Dual-layer content (technical + principle)
54    pub content: Content,
55
56    /// Knowledge tier: session → project → core
57    #[serde(default)]
58    pub tier: Tier,
59
60    /// Importance score (0.0-1.0), adjusted by feedback
61    #[serde(default = "default_importance")]
62    pub importance: f64,
63
64    /// Extraction confidence (0.0-1.0)
65    #[serde(default = "default_confidence")]
66    pub confidence: f64,
67
68    /// Classification tags
69    #[serde(default)]
70    pub tags: Tags,
71
72    /// Scope: where this knowledge applies
73    #[serde(default)]
74    pub applies: Applies,
75
76    /// Usage evidence and effectiveness tracking
77    #[serde(default)]
78    pub evidence: Evidence,
79
80    /// Connections to other knowledge items (Zettelkasten-style)
81    #[serde(default)]
82    pub links: Links,
83
84    /// Lifecycle management
85    #[serde(default)]
86    pub lifecycle: Lifecycle,
87
88    #[serde(default = "Utc::now")]
89    pub created_at: DateTime<Utc>,
90
91    #[serde(default = "Utc::now")]
92    pub updated_at: DateTime<Utc>,
93
94    /// Maturity level
95    #[serde(default)]
96    pub maturity: Maturity,
97
98    /// Decay metadata
99    #[serde(default)]
100    pub decay: DecayMeta,
101
102    /// Ownership/audience scope (Personal / Team / Community).
103    /// Orthogonal to `tier` — tier manages temporal half-life, scope manages audience.
104    #[serde(default)]
105    pub scope: Scope,
106
107    /// Monotonically increasing version counter, managed by VersionedYamlStore.
108    /// 0 means "not yet versioned" (pre-schema-3 or pre-bootstrap).
109    /// Omitted from YAML when zero so old files stay clean.
110    #[serde(default, skip_serializing_if = "is_zero_u32")]
111    pub version: u32,
112
113    /// 12-char short SHA of the knowledge-layer commit that wrote this version.
114    /// None until the pattern has been committed at least once post-schema-3.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub revision: Option<String>,
117}
118
119impl Default for KnowledgeBase {
120    fn default() -> Self {
121        Self {
122            schema: default_schema(),
123            name: String::new(),
124            description: String::new(),
125            content: Content::default(),
126            tier: Tier::default(),
127            importance: default_importance(),
128            confidence: default_confidence(),
129            tags: Tags::default(),
130            applies: Applies::default(),
131            evidence: Evidence::default(),
132            links: Links::default(),
133            lifecycle: Lifecycle::default(),
134            created_at: Utc::now(),
135            updated_at: Utc::now(),
136            maturity: Maturity::default(),
137            decay: DecayMeta::default(),
138            scope: Scope::default(),
139            version: 0,
140            revision: None,
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::pattern::Content;
149
150    #[test]
151    fn test_knowledgebase_serde_roundtrip() {
152        let kb = KnowledgeBase {
153            name: "test-kb".into(),
154            description: "A test knowledge base".into(),
155            content: Content::DualLayer {
156                technical: "Use X for Y".into(),
157                principle: Some("Because Z".into()),
158            },
159            tier: Tier::Project,
160            importance: 0.8,
161            confidence: 0.9,
162            maturity: Maturity::Stable,
163            ..Default::default()
164        };
165
166        let yaml = serde_yaml::to_string(&kb).expect("serialize");
167        let kb2: KnowledgeBase = serde_yaml::from_str(&yaml).expect("deserialize");
168
169        assert_eq!(kb2.name, "test-kb");
170        assert_eq!(kb2.description, "A test knowledge base");
171        assert_eq!(kb2.tier, Tier::Project);
172        assert!((kb2.importance - 0.8).abs() < 0.001);
173        assert!((kb2.confidence - 0.9).abs() < 0.001);
174        assert_eq!(kb2.maturity, Maturity::Stable);
175        assert_eq!(kb2.schema, 3);
176    }
177
178    #[test]
179    fn test_knowledgebase_default() {
180        let kb = KnowledgeBase::default();
181        assert_eq!(kb.schema, 3);
182        assert!((kb.confidence - 0.5).abs() < 0.001);
183        assert_eq!(kb.maturity, Maturity::Draft);
184    }
185
186    #[test]
187    fn test_knowledgebase_minimal_yaml() {
188        // Minimal YAML with just required fields; serde defaults fill the rest
189        let yaml = "name: minimal\ndescription: Minimal test\ncontent: Just text\n";
190        let kb: KnowledgeBase = serde_yaml::from_str(yaml).expect("deserialize minimal");
191        assert_eq!(kb.name, "minimal");
192        assert_eq!(kb.schema, 3); // default_schema
193        assert_eq!(kb.tier, Tier::Session);
194    }
195}