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