tuitbot_core/config/
enrichment.rs1use super::Config;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum EnrichmentStage {
12 Voice,
14 Persona,
16 Targeting,
18}
19
20impl EnrichmentStage {
21 pub fn label(self) -> &'static str {
23 match self {
24 Self::Voice => "Voice",
25 Self::Persona => "Persona",
26 Self::Targeting => "Targeting",
27 }
28 }
29
30 pub fn description(self) -> &'static str {
32 match self {
33 Self::Voice => "shapes every LLM-generated reply and tweet",
34 Self::Persona => "makes content authentic with opinions and experiences",
35 Self::Targeting => "focuses discovery on specific accounts and competitors",
36 }
37 }
38
39 pub fn all() -> &'static [EnrichmentStage] {
41 &[Self::Voice, Self::Persona, Self::Targeting]
42 }
43}
44
45impl std::fmt::Display for EnrichmentStage {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}", self.label())
48 }
49}
50
51pub struct ProfileCompleteness {
57 pub stages: Vec<(EnrichmentStage, bool)>,
59}
60
61impl ProfileCompleteness {
62 pub fn completed_count(&self) -> usize {
64 self.stages.iter().filter(|(_, done)| *done).count()
65 }
66
67 pub fn total_count(&self) -> usize {
69 self.stages.len()
70 }
71
72 pub fn is_fully_enriched(&self) -> bool {
74 self.stages.iter().all(|(_, done)| *done)
75 }
76
77 pub fn next_incomplete(&self) -> Option<EnrichmentStage> {
79 self.stages
80 .iter()
81 .find(|(_, done)| !*done)
82 .map(|(stage, _)| *stage)
83 }
84
85 pub fn one_line_summary(&self) -> String {
87 self.stages
88 .iter()
89 .map(|(stage, done)| {
90 let status = if *done { "OK" } else { "--" };
91 format!("{} {}", stage.label(), status)
92 })
93 .collect::<Vec<_>>()
94 .join(" ")
95 }
96}
97
98impl Config {
103 pub fn profile_completeness(&self) -> ProfileCompleteness {
105 let opt_non_empty =
106 |opt: &Option<String>| opt.as_ref().is_some_and(|v| !v.trim().is_empty());
107
108 let voice = opt_non_empty(&self.business.brand_voice)
109 || opt_non_empty(&self.business.reply_style)
110 || opt_non_empty(&self.business.content_style);
111
112 let persona = !self.business.persona_opinions.is_empty()
113 || !self.business.persona_experiences.is_empty()
114 || !self.business.content_pillars.is_empty();
115
116 let targeting =
117 !self.targets.accounts.is_empty() || !self.business.competitor_keywords.is_empty();
118
119 ProfileCompleteness {
120 stages: vec![
121 (EnrichmentStage::Voice, voice),
122 (EnrichmentStage::Persona, persona),
123 (EnrichmentStage::Targeting, targeting),
124 ],
125 }
126 }
127}
128
129#[cfg(test)]
134mod tests {
135 use super::*;
136
137 fn empty_config() -> Config {
138 Config::default()
139 }
140
141 #[test]
142 fn all_empty_has_zero_completeness() {
143 let config = empty_config();
144 let pc = config.profile_completeness();
145 assert_eq!(pc.completed_count(), 0);
146 assert_eq!(pc.total_count(), 3);
147 assert!(!pc.is_fully_enriched());
148 }
149
150 #[test]
151 fn voice_stage_complete_when_brand_voice_set() {
152 let mut config = empty_config();
153 config.business.brand_voice = Some("witty and concise".to_string());
154 let pc = config.profile_completeness();
155 assert_eq!(pc.completed_count(), 1);
156 assert!(pc.stages[0].1); }
158
159 #[test]
160 fn voice_stage_complete_when_reply_style_set() {
161 let mut config = empty_config();
162 config.business.reply_style = Some("helpful".to_string());
163 let pc = config.profile_completeness();
164 assert!(pc.stages[0].1);
165 }
166
167 #[test]
168 fn voice_stage_complete_when_content_style_set() {
169 let mut config = empty_config();
170 config.business.content_style = Some("educational".to_string());
171 let pc = config.profile_completeness();
172 assert!(pc.stages[0].1);
173 }
174
175 #[test]
176 fn persona_stage_complete_when_opinions_set() {
177 let mut config = empty_config();
178 config.business.persona_opinions = vec!["types are good".to_string()];
179 let pc = config.profile_completeness();
180 assert!(pc.stages[1].1); }
182
183 #[test]
184 fn targeting_stage_complete_when_accounts_set() {
185 let mut config = empty_config();
186 config.targets.accounts = vec!["elonmusk".to_string()];
187 let pc = config.profile_completeness();
188 assert!(pc.stages[2].1); }
190
191 #[test]
192 fn targeting_stage_complete_when_competitor_keywords_set() {
193 let mut config = empty_config();
194 config.business.competitor_keywords = vec!["rival_tool".to_string()];
195 let pc = config.profile_completeness();
196 assert!(pc.stages[2].1);
197 }
198
199 #[test]
200 fn empty_string_voice_not_counted() {
201 let mut config = empty_config();
202 config.business.brand_voice = Some("".to_string());
203 let pc = config.profile_completeness();
204 assert!(!pc.stages[0].1);
205 }
206
207 #[test]
208 fn whitespace_only_voice_not_counted() {
209 let mut config = empty_config();
210 config.business.brand_voice = Some(" ".to_string());
211 let pc = config.profile_completeness();
212 assert!(!pc.stages[0].1);
213 }
214
215 #[test]
216 fn fully_enriched_config() {
217 let mut config = empty_config();
218 config.business.brand_voice = Some("witty".to_string());
219 config.business.persona_opinions = vec!["opinion".to_string()];
220 config.targets.accounts = vec!["target".to_string()];
221 let pc = config.profile_completeness();
222 assert_eq!(pc.completed_count(), 3);
223 assert!(pc.is_fully_enriched());
224 assert!(pc.next_incomplete().is_none());
225 }
226
227 #[test]
228 fn next_incomplete_returns_first_missing() {
229 let mut config = empty_config();
230 config.business.brand_voice = Some("witty".to_string());
231 let pc = config.profile_completeness();
233 assert_eq!(pc.next_incomplete(), Some(EnrichmentStage::Persona));
234 }
235
236 #[test]
237 fn one_line_summary_format() {
238 let mut config = empty_config();
239 config.business.brand_voice = Some("witty".to_string());
240 config.targets.accounts = vec!["target".to_string()];
241 let pc = config.profile_completeness();
242 assert_eq!(pc.one_line_summary(), "Voice OK Persona -- Targeting OK");
243 }
244
245 #[test]
246 fn enrichment_stage_all_returns_three() {
247 assert_eq!(EnrichmentStage::all().len(), 3);
248 }
249
250 #[test]
251 fn enrichment_stage_display() {
252 assert_eq!(format!("{}", EnrichmentStage::Voice), "Voice");
253 assert_eq!(format!("{}", EnrichmentStage::Persona), "Persona");
254 assert_eq!(format!("{}", EnrichmentStage::Targeting), "Targeting");
255 }
256}