1use owo_colors::OwoColorize;
7use seshat_core::Trend;
8use seshat_detectors::AggregatedConvention;
9
10use crate::format::{self, ConfidenceTier, Verbosity, styled_tier_bullet};
11use crate::report::ReportData;
12
13const DEFAULT_TOP_N: usize = 10;
15
16pub fn print_conventions(data: &ReportData, verbosity: Verbosity, color: bool) {
28 let conventions = &data.conventions;
29 if conventions.is_empty() {
30 return;
31 }
32
33 let mut sorted: Vec<&AggregatedConvention> = conventions.iter().collect();
36 sorted.sort_by(|a, b| {
37 b.confidence
38 .partial_cmp(&a.confidence)
39 .unwrap_or(std::cmp::Ordering::Equal)
40 .then_with(|| a.description.cmp(&b.description))
41 });
42
43 let header_title = format!("Conventions Detected ({})", conventions.len());
45 eprintln!("{}", format::format_section_header(&header_title, color),);
46
47 let (high, medium, low) = count_tiers(conventions);
49 print_tier_summary(high, medium, low, color);
50
51 eprintln!();
52
53 let limit = if verbosity.show_verbose() {
55 sorted.len()
56 } else {
57 sorted.len().min(DEFAULT_TOP_N)
58 };
59
60 let max_desc_width = sorted
63 .iter()
64 .take(limit)
65 .map(|c| c.description.len())
66 .max()
67 .unwrap_or(40)
68 .clamp(30, 60);
69
70 for conv in sorted.iter().take(limit) {
71 print_convention_line(conv, color, max_desc_width);
72 }
73
74 let remaining = sorted.len().saturating_sub(limit);
76 if remaining > 0 {
77 if color {
78 eprintln!(
79 " {} and {} more — use --verbose to see all",
80 "...".dimmed(),
81 remaining,
82 );
83 } else {
84 eprintln!(" ... and {remaining} more — use --verbose to see all");
85 }
86 }
87
88 eprintln!();
89}
90
91pub fn print_next_steps(color: bool) {
100 eprintln!("{}", format::format_section_header("Next Steps", color));
101
102 let steps = [
103 "Run `seshat review` to validate detected conventions",
104 "Run `seshat serve` to start MCP server",
105 "Run `seshat init` to generate MCP config",
106 ];
107
108 for step in &steps {
109 if color {
110 eprintln!(" {}", step.dimmed());
111 } else {
112 eprintln!(" {step}");
113 }
114 }
115
116 eprintln!();
117}
118
119fn count_tiers(conventions: &[AggregatedConvention]) -> (usize, usize, usize) {
121 let mut high = 0;
122 let mut medium = 0;
123 let mut low = 0;
124
125 for conv in conventions {
126 match ConfidenceTier::from_confidence(conv.confidence * 100.0) {
127 ConfidenceTier::High => high += 1,
128 ConfidenceTier::Medium => medium += 1,
129 ConfidenceTier::Low => low += 1,
130 }
131 }
132
133 (high, medium, low)
134}
135
136fn print_tier_summary(high: usize, medium: usize, low: usize, color: bool) {
138 let parts: Vec<String> = [
139 (high, "High", ConfidenceTier::High),
140 (medium, "Medium", ConfidenceTier::Medium),
141 (low, "Low", ConfidenceTier::Low),
142 ]
143 .iter()
144 .filter(|(count, _, _)| *count > 0)
145 .map(|(count, label, tier)| format::format_tier_bullet(label, *count, *tier, color))
146 .collect();
147
148 if !parts.is_empty() {
149 eprintln!(" {}", parts.join(" "));
150 }
151}
152
153fn trend_indicator(trend: Trend) -> &'static str {
160 match trend {
161 Trend::Rising => "\u{2191}", Trend::Stable => "\u{2500}", Trend::Declining => "\u{2193}", Trend::Unknown => " ",
165 }
166}
167
168fn print_convention_line(conv: &AggregatedConvention, color: bool, desc_width: usize) {
175 let tier = ConfidenceTier::from_confidence(conv.confidence * 100.0);
176 let bullet = styled_tier_bullet(tier, color);
177
178 let pct = (conv.confidence * 100.0).round() as u32;
179 let trend = trend_indicator(conv.trend);
180 let detector = &conv.detector_name;
181
182 let desc = if conv.description.len() > desc_width {
186 let mut end = desc_width.saturating_sub(3);
187 while end > 0 && !conv.description.is_char_boundary(end) {
188 end -= 1;
189 }
190 format!("{}...", &conv.description[..end])
191 } else {
192 conv.description.clone()
193 };
194
195 if color {
196 eprintln!(
197 " {bullet} {desc:<width$} {trend} {pct:>3}% ({})",
198 detector.dimmed(),
199 width = desc_width,
200 );
201 } else {
202 eprintln!(
203 " {bullet} {desc:<width$} {trend} {pct:>3}% ({detector})",
204 width = desc_width,
205 );
206 }
207}
208
209#[cfg(test)]
214mod tests {
215 use super::*;
216 use seshat_core::{KnowledgeNature, KnowledgeWeight};
217
218 fn make_convention(
219 description: &str,
220 detector_name: &str,
221 confidence: f64,
222 trend: Trend,
223 ) -> AggregatedConvention {
224 AggregatedConvention {
225 detector_name: detector_name.to_owned(),
226 description: description.to_owned(),
227 nature: KnowledgeNature::Convention,
228 adoption_count: (confidence * 10.0) as u32,
229 total_count: 10,
230 confidence,
231 weight: KnowledgeWeight::Strong,
232 evidence: vec![],
233 trend,
234 }
235 }
236
237 fn make_report_data_with_conventions(conventions: Vec<AggregatedConvention>) -> ReportData {
238 ReportData {
239 language_breakdown: vec![],
240 total_files: 10,
241 total_dependencies: 0,
242 dependency_breakdown: vec![],
243 conventions,
244 files_discovered: 10,
245 files_parsed: 10,
246 nodes_persisted: 0,
247 edges_persisted: 0,
248 manifests_analyzed: 0,
249 docs_ingested: 0,
250 db_path: std::path::PathBuf::from("/tmp/test.db"),
251 db_size: 12_400_000,
252 elapsed: std::time::Duration::from_secs(2),
253 excluded_submodules: vec![],
254 submodules_excluded_by_flag: false,
255 }
256 }
257
258 #[test]
261 fn test_count_tiers_empty() {
262 let (high, medium, low) = count_tiers(&[]);
263 assert_eq!((high, medium, low), (0, 0, 0));
264 }
265
266 #[test]
267 fn test_count_tiers_mixed() {
268 let conventions = vec![
269 make_convention("a", "d1", 0.95, Trend::Rising), make_convention("b", "d2", 0.70, Trend::Stable), make_convention("c", "d3", 0.30, Trend::Unknown), make_convention("d", "d4", 0.90, Trend::Declining), ];
274 let (high, medium, low) = count_tiers(&conventions);
275 assert_eq!(high, 2);
276 assert_eq!(medium, 1);
277 assert_eq!(low, 1);
278 }
279
280 #[test]
281 fn test_count_tiers_boundary_85_percent() {
282 let conventions = vec![make_convention("a", "d1", 0.85, Trend::Stable)];
284 let (high, medium, _low) = count_tiers(&conventions);
285 assert_eq!(high, 0);
286 assert_eq!(medium, 1);
287 }
288
289 #[test]
290 fn test_count_tiers_boundary_50_percent() {
291 let conventions = vec![make_convention("a", "d1", 0.50, Trend::Stable)];
293 let (_high, medium, low) = count_tiers(&conventions);
294 assert_eq!(medium, 1);
295 assert_eq!(low, 0);
296 }
297
298 #[test]
301 fn test_trend_indicator_rising() {
302 assert_eq!(trend_indicator(Trend::Rising), "\u{2191}"); }
304
305 #[test]
306 fn test_trend_indicator_stable() {
307 assert_eq!(trend_indicator(Trend::Stable), "\u{2500}"); }
309
310 #[test]
311 fn test_trend_indicator_declining() {
312 assert_eq!(trend_indicator(Trend::Declining), "\u{2193}"); }
314
315 #[test]
316 fn test_trend_indicator_unknown() {
317 assert_eq!(trend_indicator(Trend::Unknown), " ");
318 }
319
320 #[test]
323 fn test_print_conventions_empty_does_not_panic() {
324 let data = make_report_data_with_conventions(vec![]);
325 print_conventions(&data, Verbosity::Default, false);
327 }
328
329 #[test]
330 fn test_print_conventions_default_mode_does_not_panic() {
331 let conventions = (0..15)
332 .map(|i| {
333 make_convention(
334 &format!("convention_{i}"),
335 "detector",
336 0.95 - (i as f64 * 0.05),
337 Trend::Stable,
338 )
339 })
340 .collect();
341 let data = make_report_data_with_conventions(conventions);
342 print_conventions(&data, Verbosity::Default, false);
344 }
345
346 #[test]
347 fn test_print_conventions_verbose_shows_all() {
348 let conventions = (0..15)
349 .map(|i| {
350 make_convention(
351 &format!("convention_{i}"),
352 "detector",
353 0.95 - (i as f64 * 0.05),
354 Trend::Rising,
355 )
356 })
357 .collect();
358 let data = make_report_data_with_conventions(conventions);
359 print_conventions(&data, Verbosity::Verbose, false);
361 }
362
363 #[test]
364 fn test_print_conventions_with_color_does_not_panic() {
365 let conventions = vec![
366 make_convention("snake_case naming", "naming", 0.98, Trend::Rising),
367 make_convention("thiserror usage", "error_handling", 0.72, Trend::Stable),
368 make_convention(
369 "test file placement",
370 "test_patterns",
371 0.30,
372 Trend::Declining,
373 ),
374 ];
375 let data = make_report_data_with_conventions(conventions);
376 print_conventions(&data, Verbosity::Default, true);
377 }
378
379 #[test]
380 fn test_print_conventions_quiet_mode() {
381 let conventions = vec![make_convention("a", "d", 0.90, Trend::Stable)];
382 let data = make_report_data_with_conventions(conventions);
383 print_conventions(&data, Verbosity::Quiet, false);
386 }
387
388 #[test]
391 fn test_print_next_steps_no_color() {
392 print_next_steps(false);
393 }
394
395 #[test]
396 fn test_print_next_steps_with_color() {
397 print_next_steps(true);
398 }
399}