Skip to main content

chronicle/cli/
knowledge.rs

1use crate::error::Result;
2use crate::git::CliOps;
3use crate::knowledge;
4use crate::schema::knowledge::{AntiPattern, Convention, KnowledgeStore, ModuleBoundary};
5use crate::schema::v2::Stability;
6
7/// Run `git chronicle knowledge list`.
8pub fn run_list(json: bool) -> Result<()> {
9    let repo_dir = std::env::current_dir().map_err(|e| crate::error::ChronicleError::Io {
10        source: e,
11        location: snafu::Location::default(),
12    })?;
13    let git_ops = CliOps::new(repo_dir);
14
15    let store = knowledge::read_store(&git_ops).map_err(|e| crate::error::ChronicleError::Git {
16        source: e,
17        location: snafu::Location::default(),
18    })?;
19
20    if json {
21        let output = serde_json::to_string_pretty(&store).map_err(|e| {
22            crate::error::ChronicleError::Json {
23                source: e,
24                location: snafu::Location::default(),
25            }
26        })?;
27        println!("{output}");
28    } else {
29        if store.is_empty() {
30            println!("No knowledge entries.");
31            return Ok(());
32        }
33        if !store.conventions.is_empty() {
34            println!("Conventions:");
35            for c in &store.conventions {
36                println!(
37                    "  [{}] scope={} stability={:?}: {}",
38                    c.id, c.scope, c.stability, c.rule
39                );
40            }
41            println!();
42        }
43        if !store.boundaries.is_empty() {
44            println!("Module boundaries:");
45            for b in &store.boundaries {
46                println!(
47                    "  [{}] {} — owns: {}, boundary: {}",
48                    b.id, b.module, b.owns, b.boundary
49                );
50            }
51            println!();
52        }
53        if !store.anti_patterns.is_empty() {
54            println!("Anti-patterns:");
55            for a in &store.anti_patterns {
56                println!(
57                    "  [{}] Don't: {} -> Instead: {}",
58                    a.id, a.pattern, a.instead
59                );
60            }
61            println!();
62        }
63    }
64
65    Ok(())
66}
67
68pub struct KnowledgeAddArgs {
69    pub entry_type: String,
70    pub id: Option<String>,
71    pub scope: Option<String>,
72    pub rule: Option<String>,
73    pub module: Option<String>,
74    pub owns: Option<String>,
75    pub boundary: Option<String>,
76    pub pattern: Option<String>,
77    pub instead: Option<String>,
78    pub stability: Option<String>,
79    pub decided_in: Option<String>,
80    pub learned_from: Option<String>,
81}
82
83/// Run `git chronicle knowledge add`.
84pub fn run_add(args: KnowledgeAddArgs) -> Result<()> {
85    let KnowledgeAddArgs {
86        entry_type,
87        id,
88        scope,
89        rule,
90        module,
91        owns,
92        boundary,
93        pattern,
94        instead,
95        stability,
96        decided_in,
97        learned_from,
98    } = args;
99    let repo_dir = std::env::current_dir().map_err(|e| crate::error::ChronicleError::Io {
100        source: e,
101        location: snafu::Location::default(),
102    })?;
103    let git_ops = CliOps::new(repo_dir);
104
105    let mut store =
106        knowledge::read_store(&git_ops).map_err(|e| crate::error::ChronicleError::Git {
107            source: e,
108            location: snafu::Location::default(),
109        })?;
110
111    match entry_type.as_str() {
112        "convention" => {
113            let scope_val = scope.ok_or_else(|| crate::error::ChronicleError::Validation {
114                message: "--scope is required for convention entries".to_string(),
115                location: snafu::Location::default(),
116            })?;
117            let rule_val = rule.ok_or_else(|| crate::error::ChronicleError::Validation {
118                message: "--rule is required for convention entries".to_string(),
119                location: snafu::Location::default(),
120            })?;
121            let stability_val = parse_stability(stability.as_deref())?;
122            let entry_id = id.unwrap_or_else(|| generate_id("conv", &store));
123            store.conventions.push(Convention {
124                id: entry_id.clone(),
125                scope: scope_val,
126                rule: rule_val,
127                decided_in,
128                stability: stability_val,
129            });
130            println!("Added convention: {entry_id}");
131        }
132        "boundary" => {
133            let module_val = module.ok_or_else(|| crate::error::ChronicleError::Validation {
134                message: "--module is required for boundary entries".to_string(),
135                location: snafu::Location::default(),
136            })?;
137            let owns_val = owns.ok_or_else(|| crate::error::ChronicleError::Validation {
138                message: "--owns is required for boundary entries".to_string(),
139                location: snafu::Location::default(),
140            })?;
141            let boundary_val =
142                boundary.ok_or_else(|| crate::error::ChronicleError::Validation {
143                    message: "--boundary is required for boundary entries".to_string(),
144                    location: snafu::Location::default(),
145                })?;
146            let entry_id = id.unwrap_or_else(|| generate_id("bound", &store));
147            store.boundaries.push(ModuleBoundary {
148                id: entry_id.clone(),
149                module: module_val,
150                owns: owns_val,
151                boundary: boundary_val,
152                decided_in,
153            });
154            println!("Added boundary: {entry_id}");
155        }
156        "anti-pattern" => {
157            let pattern_val = pattern.ok_or_else(|| crate::error::ChronicleError::Validation {
158                message: "--pattern is required for anti-pattern entries".to_string(),
159                location: snafu::Location::default(),
160            })?;
161            let instead_val = instead.ok_or_else(|| crate::error::ChronicleError::Validation {
162                message: "--instead is required for anti-pattern entries".to_string(),
163                location: snafu::Location::default(),
164            })?;
165            let entry_id = id.unwrap_or_else(|| generate_id("ap", &store));
166            store.anti_patterns.push(AntiPattern {
167                id: entry_id.clone(),
168                pattern: pattern_val,
169                instead: instead_val,
170                learned_from,
171            });
172            println!("Added anti-pattern: {entry_id}");
173        }
174        other => {
175            return Err(crate::error::ChronicleError::Validation {
176                message: format!(
177                    "Unknown entry type '{other}'. Use: convention, boundary, anti-pattern"
178                ),
179                location: snafu::Location::default(),
180            });
181        }
182    }
183
184    knowledge::write_store(&git_ops, &store).map_err(|e| crate::error::ChronicleError::Git {
185        source: e,
186        location: snafu::Location::default(),
187    })?;
188
189    Ok(())
190}
191
192/// Run `git chronicle knowledge remove`.
193pub fn run_remove(id: String) -> Result<()> {
194    let repo_dir = std::env::current_dir().map_err(|e| crate::error::ChronicleError::Io {
195        source: e,
196        location: snafu::Location::default(),
197    })?;
198    let git_ops = CliOps::new(repo_dir);
199
200    let mut store =
201        knowledge::read_store(&git_ops).map_err(|e| crate::error::ChronicleError::Git {
202            source: e,
203            location: snafu::Location::default(),
204        })?;
205
206    if store.remove_by_id(&id) {
207        knowledge::write_store(&git_ops, &store).map_err(|e| {
208            crate::error::ChronicleError::Git {
209                source: e,
210                location: snafu::Location::default(),
211            }
212        })?;
213        println!("Removed: {id}");
214    } else {
215        println!("Not found: {id}");
216    }
217
218    Ok(())
219}
220
221fn parse_stability(s: Option<&str>) -> Result<Stability> {
222    match s {
223        Some("permanent") | None => Ok(Stability::Permanent),
224        Some("provisional") => Ok(Stability::Provisional),
225        Some("experimental") => Ok(Stability::Experimental),
226        Some(other) => Err(crate::error::ChronicleError::Validation {
227            message: format!(
228                "Unknown stability '{other}'. Use: permanent, provisional, experimental"
229            ),
230            location: snafu::Location::default(),
231        }),
232    }
233}
234
235fn generate_id(prefix: &str, store: &KnowledgeStore) -> String {
236    let total = store.conventions.len() + store.boundaries.len() + store.anti_patterns.len();
237    format!("{prefix}-{}", total + 1)
238}