1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::borrow::Cow;
4use std::collections::HashMap;
5
6use crate::knowledge::KnowledgeBase;
7
8pub const SCHEMA_VERSION: u32 = 3;
10
11#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "lowercase")]
14pub enum PatternKind {
15 #[default]
17 Technical,
18 Preference,
20 Fact,
22 Procedure,
24 Behavioral,
26}
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
30#[serde(rename_all = "snake_case")]
31pub enum OriginTrigger {
32 UserExplicit,
34 UserCorrection,
36 AgentInferred,
38 CommunityShared,
40 AutoConsolidated,
42 Automatic,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Origin {
49 pub source: String,
51 pub trigger: OriginTrigger,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub actor: Option<crate::Actor>,
58
59 #[deprecated(note = "use actor instead, removed in v2.3")]
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub user: Option<String>,
63
64 #[deprecated(note = "use actor.source instead, removed in v2.3")]
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub platform: Option<String>,
68
69 #[serde(default = "default_origin_confidence")]
71 pub confidence: f64,
72}
73
74fn default_origin_confidence() -> f64 {
75 1.0
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Pattern {
86 #[serde(flatten)]
88 pub base: KnowledgeBase,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub kind: Option<PatternKind>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub origin: Option<Origin>,
98
99 #[serde(default)]
101 pub attachments: Vec<Attachment>,
102}
103
104impl Pattern {
105 pub fn effective_kind(&self) -> PatternKind {
107 self.kind.unwrap_or(PatternKind::Technical)
108 }
109}
110
111impl std::ops::Deref for Pattern {
113 type Target = KnowledgeBase;
114 fn deref(&self) -> &KnowledgeBase {
115 &self.base
116 }
117}
118impl std::ops::DerefMut for Pattern {
119 fn deref_mut(&mut self) -> &mut KnowledgeBase {
120 &mut self.base
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct Attachment {
127 #[serde(rename = "type")]
129 pub att_type: AttachmentType,
130 pub format: AttachmentFormat,
132 pub path: String,
134 #[serde(default)]
136 pub description: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140#[serde(rename_all = "lowercase")]
141pub enum AttachmentType {
142 Diagram,
143 Image,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147#[serde(rename_all = "lowercase")]
148pub enum AttachmentFormat {
149 Mermaid,
150 #[serde(rename = "plantuml")]
151 PlantUml,
152 Png,
153 Svg,
154}
155
156impl AttachmentFormat {
157 pub fn is_text_based(&self) -> bool {
159 matches!(self, AttachmentFormat::Mermaid | AttachmentFormat::PlantUml)
160 }
161
162 pub fn from_extension(ext: &str) -> Option<Self> {
164 match ext.to_lowercase().as_str() {
165 "mmd" | "mermaid" => Some(AttachmentFormat::Mermaid),
166 "puml" | "plantuml" => Some(AttachmentFormat::PlantUml),
167 "png" => Some(AttachmentFormat::Png),
168 "svg" => Some(AttachmentFormat::Svg),
169 _ => None,
170 }
171 }
172
173 pub fn fence_lang(&self) -> &str {
175 match self {
176 AttachmentFormat::Mermaid => "mermaid",
177 AttachmentFormat::PlantUml => "plantuml",
178 _ => "",
179 }
180 }
181}
182
183impl AttachmentType {
184 pub fn from_format(format: &AttachmentFormat) -> Self {
186 match format {
187 AttachmentFormat::Mermaid | AttachmentFormat::PlantUml => AttachmentType::Diagram,
188 AttachmentFormat::Png | AttachmentFormat::Svg => AttachmentType::Image,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(untagged)]
197pub enum Content {
198 DualLayer {
200 technical: String,
201 #[serde(default)]
202 principle: Option<String>,
203 },
204 Plain(String),
206}
207
208impl Default for Content {
209 fn default() -> Self {
210 Content::Plain(String::new())
211 }
212}
213
214impl Content {
215 pub fn as_text(&self) -> Cow<'_, str> {
220 match self {
221 Content::DualLayer {
222 technical,
223 principle,
224 } => match principle {
225 Some(p) => Cow::Owned(format!("{}\n\n{}", technical, p)),
226 None => Cow::Borrowed(technical),
227 },
228 Content::Plain(s) => Cow::Borrowed(s),
229 }
230 }
231
232 pub const MAX_LAYER_CHARS: usize = 500;
234}
235
236#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
237#[serde(rename_all = "lowercase")]
238pub enum Tier {
239 #[default]
241 Session,
242 Project,
244 Core,
246}
247
248impl Tier {
249 pub fn decay_half_life_days(&self) -> u32 {
251 match self {
252 Tier::Session => 14,
253 Tier::Project => 90,
254 Tier::Core => 365,
255 }
256 }
257}
258
259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260pub struct Tags {
261 #[serde(default)]
262 pub languages: Vec<String>,
263 #[serde(default)]
264 pub topics: Vec<String>,
265 #[serde(flatten)]
267 pub extra: HashMap<String, Vec<String>>,
268}
269
270#[derive(Debug, Clone, Default, Serialize, Deserialize)]
271pub struct Applies {
272 #[serde(default)]
274 pub projects: Vec<String>,
275 #[serde(default)]
276 pub languages: Vec<String>,
277 #[serde(default)]
279 pub tools: Vec<String>,
280 #[serde(default)]
282 pub auto_scope: bool,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub struct Contribution {
292 #[serde(default)]
293 pub success_signals: u64,
294 #[serde(default)]
295 pub override_signals: u64,
296 pub last_seen: DateTime<Utc>,
297}
298
299#[derive(Debug, Clone, Default, Serialize, Deserialize)]
300pub struct Evidence {
301 #[serde(default)]
302 pub source_sessions: Vec<String>,
303 pub first_seen: Option<DateTime<Utc>>,
304 pub last_validated: Option<DateTime<Utc>>,
305 #[serde(default)]
306 pub injection_count: u64,
307 #[serde(default)]
308 pub success_signals: u64,
309 #[serde(default)]
310 pub failure_signals: u64,
311 #[serde(default)]
312 pub override_signals: u64,
313 #[serde(default)]
316 pub contributions: HashMap<String, Contribution>,
317}
318
319impl Evidence {
320 pub fn effectiveness(&self) -> f64 {
322 let total = self.success_signals + self.override_signals;
323 if total == 0 {
324 0.5 } else {
326 self.success_signals as f64 / total as f64
327 }
328 }
329
330 pub fn effectiveness_by_actor(&self, actor: &crate::Actor) -> f64 {
336 match self.contributions.get(&actor.key()) {
337 Some(c) => {
338 let total = c.success_signals + c.override_signals;
339 if total == 0 {
340 0.5 } else {
342 c.success_signals as f64 / total as f64
343 }
344 }
345 None => 0.5, }
347 }
348}
349
350#[derive(Debug, Clone, Default, Serialize, Deserialize)]
351pub struct Links {
352 #[serde(default)]
354 pub related: Vec<String>,
355 #[serde(default)]
357 pub supersedes: Vec<String>,
358 #[serde(default)]
360 pub workflows: Vec<String>,
361}
362
363#[derive(Debug, Clone, Default, Serialize, Deserialize)]
364pub struct Lifecycle {
365 #[serde(default)]
366 pub status: LifecycleStatus,
367 pub decay_half_life: Option<u32>,
369 pub last_injected: Option<DateTime<Utc>>,
370 #[serde(default)]
372 pub pinned: bool,
373 #[serde(default)]
375 pub muted: bool,
376}
377
378#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
379#[serde(rename_all = "lowercase")]
380pub enum LifecycleStatus {
381 #[default]
382 Active,
383 Deprecated,
384 Archived,
385}
386
387pub fn default_schema() -> u32 {
388 SCHEMA_VERSION
389}
390pub fn default_importance() -> f64 {
391 0.5
392}
393pub fn default_confidence() -> f64 {
394 0.5
395}
396
397#[cfg(test)]
398mod tests;