Skip to main content

agent_docs/
output.rs

1use anyhow::{Context, Result};
2use serde::Serialize;
3
4use crate::commands::scaffold_baseline::ScaffoldBaselineReport;
5use crate::model::{
6    BaselineCheckReport, Context as DocContext, OutputFormat, ResolveFormat, ResolveReport,
7    StubReport,
8};
9
10#[derive(Debug, Serialize)]
11struct ContextsOutput<'a> {
12    contexts: &'a [DocContext],
13}
14
15pub fn render_contexts(format: ResolveFormat, contexts: &[DocContext]) -> Result<String> {
16    match format {
17        ResolveFormat::Text => Ok(contexts
18            .iter()
19            .map(ToString::to_string)
20            .collect::<Vec<_>>()
21            .join("\n")),
22        ResolveFormat::Json => serde_json::to_string_pretty(&ContextsOutput { contexts })
23            .context("failed to serialize contexts output"),
24        ResolveFormat::Checklist => Ok(render_contexts_checklist(contexts)),
25    }
26}
27
28fn render_contexts_checklist(contexts: &[DocContext]) -> String {
29    let total = contexts.len();
30    let mut lines = Vec::with_capacity(total + 2);
31    lines.push(format!("CONTEXTS_BEGIN total={total}"));
32    for ctx in contexts {
33        lines.push(ctx.to_string());
34    }
35    lines.push(format!("CONTEXTS_END total={total}"));
36    lines.join("\n")
37}
38
39pub fn render_resolve(format: ResolveFormat, report: &ResolveReport) -> Result<String> {
40    match format {
41        ResolveFormat::Text => Ok(render_resolve_text(report)),
42        ResolveFormat::Json => {
43            serde_json::to_string_pretty(report).context("failed to serialize resolve output")
44        }
45        ResolveFormat::Checklist => Ok(render_resolve_checklist(report)),
46    }
47}
48
49pub fn render_stub(
50    format: OutputFormat,
51    command: &str,
52    message: impl Into<String>,
53) -> Result<String> {
54    let report = StubReport {
55        command: command.to_string(),
56        implemented: false,
57        message: message.into(),
58    };
59
60    match format {
61        OutputFormat::Text => Ok(format!("{}: {}", report.command, report.message)),
62        OutputFormat::Json => {
63            serde_json::to_string_pretty(&report).context("failed to serialize stub output")
64        }
65    }
66}
67
68pub fn render_baseline(format: OutputFormat, report: &BaselineCheckReport) -> Result<String> {
69    match format {
70        OutputFormat::Text => Ok(render_baseline_text(report)),
71        OutputFormat::Json => {
72            serde_json::to_string_pretty(report).context("failed to serialize baseline output")
73        }
74    }
75}
76
77pub fn render_scaffold_baseline(
78    format: OutputFormat,
79    report: &ScaffoldBaselineReport,
80) -> Result<String> {
81    match format {
82        OutputFormat::Text => Ok(render_scaffold_baseline_text(report)),
83        OutputFormat::Json => serde_json::to_string_pretty(report)
84            .context("failed to serialize scaffold-baseline output"),
85    }
86}
87
88fn render_resolve_text(report: &ResolveReport) -> String {
89    let mut lines = Vec::new();
90    lines.push(format!("CONTEXT: {}", report.context));
91    lines.push(format!("AGENT_HOME: {}", report.agent_home.display()));
92    lines.push(format!("PROJECT_PATH: {}", report.project_path.display()));
93    lines.push(String::new());
94
95    for doc in &report.documents {
96        let required_label = if doc.required { "required" } else { "optional" };
97        lines.push(format!(
98            "[{}] {} {} {} source={} status={} why=\"{}\"",
99            required_label,
100            doc.context,
101            doc.scope,
102            doc.path.display(),
103            doc.source,
104            doc.status,
105            doc.why
106        ));
107    }
108
109    lines.push(String::new());
110    lines.push(format!(
111        "summary: required_total={} present_required={} missing_required={} strict={}",
112        report.summary.required_total,
113        report.summary.present_required,
114        report.summary.missing_required,
115        report.strict
116    ));
117
118    lines.join("\n")
119}
120
121fn render_resolve_checklist(report: &ResolveReport) -> String {
122    let mode = if report.strict {
123        "strict"
124    } else {
125        "non-strict"
126    };
127    let mut lines = Vec::new();
128    lines.push(format!(
129        "REQUIRED_DOCS_BEGIN context={} mode={mode}",
130        report.context
131    ));
132
133    for doc in report.documents.iter().filter(|doc| doc.required) {
134        let file_name = doc
135            .path
136            .file_name()
137            .and_then(|name| name.to_str())
138            .map(ToOwned::to_owned)
139            .unwrap_or_else(|| doc.path.display().to_string());
140        lines.push(format!(
141            "{} status={} path={}",
142            file_name,
143            doc.status,
144            doc.path.display()
145        ));
146    }
147
148    lines.push(format!(
149        "REQUIRED_DOCS_END required={} present={} missing={} mode={mode} context={}",
150        report.summary.required_total,
151        report.summary.present_required,
152        report.summary.missing_required,
153        report.context
154    ));
155
156    lines.join("\n")
157}
158
159fn render_baseline_text(report: &BaselineCheckReport) -> String {
160    let mut lines = Vec::new();
161    lines.push(format!("BASELINE CHECK: {}", report.target));
162    lines.push(format!("AGENT_HOME: {}", report.agent_home.display()));
163    lines.push(format!("PROJECT_PATH: {}", report.project_path.display()));
164    lines.push(String::new());
165
166    for item in &report.items {
167        let required_label = if item.required {
168            "required"
169        } else {
170            "optional"
171        };
172        lines.push(format!(
173            "[{}] {:<15} {} {} {} source={} why=\"{}\"",
174            item.scope,
175            item.label,
176            item.path.display(),
177            required_label,
178            item.status,
179            item.source,
180            item.why
181        ));
182    }
183
184    lines.push(String::new());
185    lines.push(format!("missing_required: {}", report.missing_required));
186    lines.push(format!("missing_optional: {}", report.missing_optional));
187    lines.push("suggested_actions:".to_string());
188    if report.suggested_actions.is_empty() {
189        lines.push("  - (none)".to_string());
190    } else {
191        lines.extend(
192            report
193                .suggested_actions
194                .iter()
195                .map(|action| format!("  - {action}")),
196        );
197    }
198
199    lines.join("\n")
200}
201
202fn render_scaffold_baseline_text(report: &ScaffoldBaselineReport) -> String {
203    let mut lines = Vec::new();
204    lines.push(format!("SCAFFOLD BASELINE: {}", report.target));
205    lines.push(format!("AGENT_HOME: {}", report.agent_home.display()));
206    lines.push(format!("PROJECT_PATH: {}", report.project_path.display()));
207    lines.push(String::new());
208
209    for item in &report.items {
210        lines.push(format!(
211            "[{}] {:<15} {} action={} reason=\"{}\"",
212            item.scope,
213            item.label,
214            item.path.display(),
215            item.action,
216            item.reason
217        ));
218    }
219
220    lines.push(String::new());
221    lines.push(format!(
222        "summary: created={} overwritten={} skipped={} planned_create={} planned_overwrite={} planned_skip={}",
223        report.created,
224        report.overwritten,
225        report.skipped,
226        report.planned_create,
227        report.planned_overwrite,
228        report.planned_skip
229    ));
230
231    lines.join("\n")
232}