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}