Skip to main content

chronicle/show/
plain.rs

1use std::io::Write;
2
3use super::data::ShowData;
4
5/// Render annotated file as plain text. Used when stdout is not a TTY or --no-tui.
6pub fn run_plain(data: &ShowData, w: &mut dyn Write) -> std::io::Result<()> {
7    writeln!(
8        w,
9        "{} @ {}",
10        data.file_path,
11        &data.commit[..7.min(data.commit.len())]
12    )?;
13    writeln!(w)?;
14
15    if data.regions.is_empty() {
16        writeln!(w, "  (no annotations)")?;
17        return Ok(());
18    }
19
20    for r in &data.regions {
21        // Region header
22        writeln!(
23            w,
24            "  {}-{}  {} ({})",
25            r.region.lines.start,
26            r.region.lines.end,
27            r.region.ast_anchor.name,
28            r.region.ast_anchor.unit_type,
29        )?;
30
31        // Intent (always)
32        writeln!(w, "        intent:  {}", r.region.intent)?;
33
34        // Reasoning
35        if let Some(ref reasoning) = r.region.reasoning {
36            writeln!(w, "        reasoning: {reasoning}")?;
37        }
38
39        // Constraints
40        if !r.region.constraints.is_empty() {
41            writeln!(w, "        constraints:")?;
42            for c in &r.region.constraints {
43                let source = match c.source {
44                    crate::schema::v1::ConstraintSource::Author => "author",
45                    crate::schema::v1::ConstraintSource::Inferred => "inferred",
46                };
47                writeln!(w, "          - {} [{source}]", c.text)?;
48            }
49        }
50
51        // Semantic dependencies
52        if !r.region.semantic_dependencies.is_empty() {
53            writeln!(w, "        deps:")?;
54            for d in &r.region.semantic_dependencies {
55                writeln!(w, "          -> {} :: {}", d.file, d.anchor)?;
56            }
57        }
58
59        // Risk notes
60        if let Some(ref risk) = r.region.risk_notes {
61            writeln!(w, "        risk: {risk}")?;
62        }
63
64        // Corrections
65        if !r.region.corrections.is_empty() {
66            writeln!(w, "        corrections: {}", r.region.corrections.len())?;
67        }
68
69        writeln!(w)?;
70    }
71
72    Ok(())
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::schema::common::*;
79    use crate::schema::v1::*;
80    use crate::show::data::RegionRef;
81
82    fn make_test_data() -> ShowData {
83        ShowData {
84            file_path: "src/main.rs".to_string(),
85            commit: "abc1234567890".to_string(),
86            source_lines: vec!["fn main() {}".to_string()],
87            regions: vec![RegionRef {
88                region: RegionAnnotation {
89                    file: "src/main.rs".to_string(),
90                    ast_anchor: AstAnchor {
91                        unit_type: "function".to_string(),
92                        name: "main".to_string(),
93                        signature: None,
94                    },
95                    lines: LineRange { start: 1, end: 1 },
96                    intent: "Entry point".to_string(),
97                    reasoning: Some("Standard main".to_string()),
98                    constraints: vec![Constraint {
99                        text: "Must not panic".to_string(),
100                        source: ConstraintSource::Author,
101                    }],
102                    semantic_dependencies: vec![SemanticDependency {
103                        file: "src/lib.rs".to_string(),
104                        anchor: "run".to_string(),
105                        nature: "calls".to_string(),
106                    }],
107                    related_annotations: vec![],
108                    tags: vec![],
109                    risk_notes: Some("None currently".to_string()),
110                    corrections: vec![],
111                },
112                commit: "abc1234567890".to_string(),
113                timestamp: "2025-01-01T00:00:00Z".to_string(),
114                summary: "test".to_string(),
115                context_level: ContextLevel::Inferred,
116                provenance: Provenance {
117                    operation: ProvenanceOperation::Initial,
118                    derived_from: vec![],
119                    original_annotations_preserved: false,
120                    synthesis_notes: None,
121                },
122            }],
123            annotation_map: crate::show::data::LineAnnotationMap::build_from_regions(&[], 1),
124        }
125    }
126
127    #[test]
128    fn test_plain_output_contains_intent() {
129        let data = make_test_data();
130        let mut buf = Vec::new();
131        run_plain(&data, &mut buf).unwrap();
132        let output = String::from_utf8(buf).unwrap();
133        assert!(output.contains("intent:  Entry point"));
134    }
135
136    #[test]
137    fn test_plain_output_contains_reasoning() {
138        let data = make_test_data();
139        let mut buf = Vec::new();
140        run_plain(&data, &mut buf).unwrap();
141        let output = String::from_utf8(buf).unwrap();
142        assert!(output.contains("reasoning: Standard main"));
143    }
144
145    #[test]
146    fn test_plain_output_contains_constraints() {
147        let data = make_test_data();
148        let mut buf = Vec::new();
149        run_plain(&data, &mut buf).unwrap();
150        let output = String::from_utf8(buf).unwrap();
151        assert!(output.contains("Must not panic [author]"));
152    }
153
154    #[test]
155    fn test_plain_output_contains_deps() {
156        let data = make_test_data();
157        let mut buf = Vec::new();
158        run_plain(&data, &mut buf).unwrap();
159        let output = String::from_utf8(buf).unwrap();
160        assert!(output.contains("-> src/lib.rs :: run"));
161    }
162
163    #[test]
164    fn test_plain_output_empty_annotations() {
165        let data = ShowData {
166            file_path: "src/empty.rs".to_string(),
167            commit: "abc1234".to_string(),
168            source_lines: vec![],
169            regions: vec![],
170            annotation_map: crate::show::data::LineAnnotationMap::build_from_regions(&[], 0),
171        };
172        let mut buf = Vec::new();
173        run_plain(&data, &mut buf).unwrap();
174        let output = String::from_utf8(buf).unwrap();
175        assert!(output.contains("(no annotations)"));
176    }
177}