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}