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
7pub 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
83pub 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
192pub 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}