ta-changeset 0.15.15-alpha.3

ChangeSet and PR Package data model for Trusted Autonomy
Documentation
//! markdown.rs — Markdown output adapter for GitHub PR bodies.

use crate::error::ChangeSetError;
use crate::output_adapters::{
    default_summary, matches_file_filters, DetailLevel, OutputAdapter, RenderContext,
};
use crate::pr_package::{Artifact, ChangeType};

#[derive(Default)]
pub struct MarkdownAdapter {}

impl MarkdownAdapter {
    pub fn new() -> Self {
        Self {}
    }

    fn change_icon(&self, change_type: &ChangeType) -> &str {
        match change_type {
            ChangeType::Add => "",
            ChangeType::Modify => "✏️",
            ChangeType::Delete => "🗑️",
            ChangeType::Rename => "📝",
        }
    }
}

impl OutputAdapter for MarkdownAdapter {
    fn render(&self, ctx: &RenderContext) -> Result<String, ChangeSetError> {
        let pkg = ctx.package;
        let mut output = String::new();

        // Header
        output.push_str(&format!("# Draft: {}\n\n", pkg.package_id));
        output.push_str(&format!("**Status**: {}\n\n", pkg.status));
        output.push_str(&format!("**Goal**: {}\n\n", pkg.goal.title));
        output.push_str(&format!(
            "**Created**: {}\n\n",
            pkg.created_at.format("%Y-%m-%d %H:%M:%S")
        ));

        // Summary
        output.push_str("## Summary\n\n");
        output.push_str(&format!(
            "**What changed**: {}\n\n",
            pkg.summary.what_changed
        ));
        output.push_str(&format!("**Why**: {}\n\n", pkg.summary.why));
        output.push_str(&format!("**Impact**: {}\n\n", pkg.summary.impact));

        // Changes
        output.push_str(&format!(
            "## Changes ({} artifacts)\n\n",
            pkg.changes.artifacts.len()
        ));

        let artifacts: Vec<&Artifact> = pkg
            .changes
            .artifacts
            .iter()
            .filter(|a| matches_file_filters(&a.resource_uri, &ctx.file_filters))
            .collect();

        for artifact in artifacts {
            let icon = self.change_icon(&artifact.change_type);

            match ctx.detail_level {
                DetailLevel::Top => {
                    let summary = artifact
                        .explanation_tiers
                        .as_ref()
                        .map(|t| t.summary.as_str())
                        .or(artifact.rationale.as_deref())
                        .unwrap_or_else(|| {
                            default_summary(&artifact.resource_uri, &artifact.change_type)
                        });
                    output.push_str(&format!(
                        "- {} **{}** — {}\n",
                        icon, artifact.resource_uri, summary
                    ));
                }
                DetailLevel::Medium | DetailLevel::Full => {
                    output.push_str(&format!("\n### {} {}\n\n", icon, artifact.resource_uri));

                    if let Some(tiers) = &artifact.explanation_tiers {
                        output.push_str(&format!("**Summary**: {}\n\n", tiers.summary));
                        output.push_str(&format!("{}\n\n", tiers.explanation));

                        if !tiers.tags.is_empty() {
                            output.push_str(&format!("**Tags**: {}\n\n", tiers.tags.join(", ")));
                        }

                        if !tiers.related_artifacts.is_empty() {
                            output.push_str("**Related artifacts**:\n");
                            for related in &tiers.related_artifacts {
                                output.push_str(&format!("- {}\n", related));
                            }
                            output.push('\n');
                        }
                    } else if let Some(rationale) = &artifact.rationale {
                        output.push_str(&format!("**Rationale**: {}\n\n", rationale));
                    }

                    if ctx.detail_level == DetailLevel::Full {
                        if let Some(provider) = ctx.diff_provider {
                            match provider.get_diff(&artifact.diff_ref) {
                                Ok(diff) => {
                                    output.push_str(
                                        "<details>\n<summary>View diff</summary>\n\n```diff\n",
                                    );
                                    output.push_str(&diff);
                                    output.push_str("\n```\n</details>\n\n");
                                }
                                Err(_) => {
                                    output.push_str(&format!("*Diff: {}*\n\n", artifact.diff_ref));
                                }
                            }
                        }
                    }
                }
            }
        }

        // Footer
        output.push_str("\n---\n\n");
        output.push_str(&format!(
            "🤖 Generated by Trusted Autonomy v{}\n",
            pkg.package_version
        ));

        Ok(output)
    }

    fn name(&self) -> &str {
        "markdown"
    }
}