Skip to main content

ta_changeset/output_adapters/
markdown.rs

1//! markdown.rs — Markdown output adapter for GitHub PR bodies.
2
3use crate::error::ChangeSetError;
4use crate::output_adapters::{
5    default_summary, matches_file_filters, DetailLevel, OutputAdapter, RenderContext,
6};
7use crate::pr_package::{Artifact, ChangeType};
8
9#[derive(Default)]
10pub struct MarkdownAdapter {}
11
12impl MarkdownAdapter {
13    pub fn new() -> Self {
14        Self {}
15    }
16
17    fn change_icon(&self, change_type: &ChangeType) -> &str {
18        match change_type {
19            ChangeType::Add => "➕",
20            ChangeType::Modify => "✏️",
21            ChangeType::Delete => "🗑️",
22            ChangeType::Rename => "📝",
23        }
24    }
25}
26
27impl OutputAdapter for MarkdownAdapter {
28    fn render(&self, ctx: &RenderContext) -> Result<String, ChangeSetError> {
29        let pkg = ctx.package;
30        let mut output = String::new();
31
32        // Header
33        output.push_str(&format!("# Draft: {}\n\n", pkg.package_id));
34        output.push_str(&format!("**Status**: {}\n\n", pkg.status));
35        output.push_str(&format!("**Goal**: {}\n\n", pkg.goal.title));
36        output.push_str(&format!(
37            "**Created**: {}\n\n",
38            pkg.created_at.format("%Y-%m-%d %H:%M:%S")
39        ));
40
41        // Summary
42        output.push_str("## Summary\n\n");
43        output.push_str(&format!(
44            "**What changed**: {}\n\n",
45            pkg.summary.what_changed
46        ));
47        output.push_str(&format!("**Why**: {}\n\n", pkg.summary.why));
48        output.push_str(&format!("**Impact**: {}\n\n", pkg.summary.impact));
49
50        // Changes
51        output.push_str(&format!(
52            "## Changes ({} artifacts)\n\n",
53            pkg.changes.artifacts.len()
54        ));
55
56        let artifacts: Vec<&Artifact> = pkg
57            .changes
58            .artifacts
59            .iter()
60            .filter(|a| matches_file_filters(&a.resource_uri, &ctx.file_filters))
61            .collect();
62
63        for artifact in artifacts {
64            let icon = self.change_icon(&artifact.change_type);
65
66            match ctx.detail_level {
67                DetailLevel::Top => {
68                    let summary = artifact
69                        .explanation_tiers
70                        .as_ref()
71                        .map(|t| t.summary.as_str())
72                        .or(artifact.rationale.as_deref())
73                        .unwrap_or_else(|| {
74                            default_summary(&artifact.resource_uri, &artifact.change_type)
75                        });
76                    output.push_str(&format!(
77                        "- {} **{}** — {}\n",
78                        icon, artifact.resource_uri, summary
79                    ));
80                }
81                DetailLevel::Medium | DetailLevel::Full => {
82                    output.push_str(&format!("\n### {} {}\n\n", icon, artifact.resource_uri));
83
84                    if let Some(tiers) = &artifact.explanation_tiers {
85                        output.push_str(&format!("**Summary**: {}\n\n", tiers.summary));
86                        output.push_str(&format!("{}\n\n", tiers.explanation));
87
88                        if !tiers.tags.is_empty() {
89                            output.push_str(&format!("**Tags**: {}\n\n", tiers.tags.join(", ")));
90                        }
91
92                        if !tiers.related_artifacts.is_empty() {
93                            output.push_str("**Related artifacts**:\n");
94                            for related in &tiers.related_artifacts {
95                                output.push_str(&format!("- {}\n", related));
96                            }
97                            output.push('\n');
98                        }
99                    } else if let Some(rationale) = &artifact.rationale {
100                        output.push_str(&format!("**Rationale**: {}\n\n", rationale));
101                    }
102
103                    if ctx.detail_level == DetailLevel::Full {
104                        if let Some(provider) = ctx.diff_provider {
105                            match provider.get_diff(&artifact.diff_ref) {
106                                Ok(diff) => {
107                                    output.push_str(
108                                        "<details>\n<summary>View diff</summary>\n\n```diff\n",
109                                    );
110                                    output.push_str(&diff);
111                                    output.push_str("\n```\n</details>\n\n");
112                                }
113                                Err(_) => {
114                                    output.push_str(&format!("*Diff: {}*\n\n", artifact.diff_ref));
115                                }
116                            }
117                        }
118                    }
119                }
120            }
121        }
122
123        // Footer
124        output.push_str("\n---\n\n");
125        output.push_str(&format!(
126            "🤖 Generated by Trusted Autonomy v{}\n",
127            pkg.package_version
128        ));
129
130        Ok(output)
131    }
132
133    fn name(&self) -> &str {
134        "markdown"
135    }
136}