1use crate::root_envelopes::{RootEnvelopeMode, attach_telemetry_meta, serialize_named_json_output};
4use serde::Serialize;
5
6#[derive(Debug, Clone, Serialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[cfg_attr(
15 feature = "schema",
16 schemars(title = "fallow explain <issue-type> --format json")
17)]
18pub struct ExplainOutput {
19 pub id: String,
20 pub name: String,
21 pub summary: String,
22 pub rationale: String,
23 pub example: String,
24 pub how_to_fix: String,
25 pub docs: String,
26}
27
28pub fn serialize_explain_json_output(
34 output: ExplainOutput,
35 mode: RootEnvelopeMode,
36 analysis_run_id: Option<&str>,
37) -> Result<serde_json::Value, serde_json::Error> {
38 let mut value = serialize_named_json_output(output, "explain", mode)?;
39 attach_telemetry_meta(&mut value, analysis_run_id);
40 Ok(value)
41}
42
43#[derive(Debug, Clone, Serialize)]
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46#[cfg_attr(feature = "schema", schemars(title = "fallow inspect --format json"))]
47pub struct InspectOutput {
48 pub target: InspectTargetDescriptor,
49 pub identity: InspectIdentity,
50 pub evidence: InspectEvidence,
51 pub warnings: Vec<String>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum InspectTargetDescriptor {
58 File { file: String },
59 Symbol { file: String, export_name: String },
60}
61
62#[derive(Debug, Clone, Serialize)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[serde(untagged)]
65pub enum InspectIdentity {
66 File(InspectFileIdentity),
67 Symbol(InspectSymbolIdentity),
68}
69
70#[derive(Debug, Clone, Serialize)]
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72pub struct InspectFileIdentity {
73 pub file: String,
74 pub is_reachable: Option<serde_json::Value>,
75 pub is_entry_point: Option<serde_json::Value>,
76 pub export_count: Option<usize>,
77 pub import_count: Option<usize>,
78 pub imported_by_count: Option<usize>,
79}
80
81#[derive(Debug, Clone, Serialize)]
82#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
83pub struct InspectSymbolIdentity {
84 pub file: String,
85 pub export_name: String,
86 pub file_reachable: Option<serde_json::Value>,
87 pub is_entry_point: Option<serde_json::Value>,
88 pub is_used: Option<serde_json::Value>,
89 pub reason: Option<serde_json::Value>,
90}
91
92#[derive(Debug, Clone, Serialize)]
93#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
94pub struct InspectEvidence {
95 pub trace_file: InspectEvidenceSection,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub trace_export: Option<InspectEvidenceSection>,
98 pub dead_code: InspectEvidenceSection,
99 pub duplication: InspectEvidenceSection,
100 pub complexity: InspectEvidenceSection,
101 pub security: InspectEvidenceSection,
102 pub impact_closure: InspectEvidenceSection,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub churn: Option<InspectEvidenceSection>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub symbol_chain: Option<InspectEvidenceSection>,
115}
116
117#[derive(Debug, Clone, Serialize)]
118#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
119pub struct InspectEvidenceSection {
120 pub status: InspectSectionStatus,
121 pub scope: InspectEvidenceScope,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub message: Option<String>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub data: Option<serde_json::Value>,
126}
127
128impl InspectEvidenceSection {
129 #[must_use]
130 pub fn ok(scope: InspectEvidenceScope, data: serde_json::Value) -> Self {
131 Self {
132 status: InspectSectionStatus::Ok,
133 scope,
134 message: None,
135 data: Some(data),
136 }
137 }
138
139 #[must_use]
140 pub fn error(scope: InspectEvidenceScope, message: String) -> Self {
141 Self {
142 status: InspectSectionStatus::Error,
143 scope,
144 message: Some(message),
145 data: None,
146 }
147 }
148
149 #[must_use]
150 pub fn unavailable(scope: InspectEvidenceScope, message: String) -> Self {
151 Self {
152 status: InspectSectionStatus::Unavailable,
153 scope,
154 message: Some(message),
155 data: None,
156 }
157 }
158}
159
160pub fn serialize_inspect_json_output(
166 output: InspectOutput,
167 mode: RootEnvelopeMode,
168 analysis_run_id: Option<&str>,
169) -> Result<serde_json::Value, serde_json::Error> {
170 let mut value = serialize_named_json_output(output, "inspect_target", mode)?;
171 attach_telemetry_meta(&mut value, analysis_run_id);
172 Ok(value)
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177#[serde(rename_all = "snake_case")]
178pub enum InspectSectionStatus {
179 Ok,
180 Unavailable,
181 Error,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
186#[serde(rename_all = "snake_case")]
187pub enum InspectEvidenceScope {
188 Symbol,
189 File,
190 ProjectFilteredToFile,
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn explain_json_output_uses_output_owned_root_contract() {
199 let output = ExplainOutput {
200 id: "unused-export".to_string(),
201 name: "Unused export".to_string(),
202 summary: "summary".to_string(),
203 rationale: "rationale".to_string(),
204 example: "example".to_string(),
205 how_to_fix: "fix".to_string(),
206 docs: "https://example.test".to_string(),
207 };
208
209 let value =
210 serialize_explain_json_output(output, RootEnvelopeMode::Tagged, Some("run-explain"))
211 .expect("explain output should serialize");
212
213 assert_eq!(value["kind"], "explain");
214 assert_eq!(
215 value["_meta"]["telemetry"]["analysis_run_id"],
216 "run-explain"
217 );
218 }
219
220 #[test]
221 fn inspect_json_output_uses_output_owned_root_contract() {
222 let output = InspectOutput {
223 target: InspectTargetDescriptor::File {
224 file: "src/app.ts".to_string(),
225 },
226 identity: InspectIdentity::File(InspectFileIdentity {
227 file: "src/app.ts".to_string(),
228 is_reachable: None,
229 is_entry_point: None,
230 export_count: Some(0),
231 import_count: Some(0),
232 imported_by_count: Some(0),
233 }),
234 evidence: InspectEvidence {
235 trace_file: InspectEvidenceSection::ok(
236 InspectEvidenceScope::File,
237 serde_json::json!({}),
238 ),
239 trace_export: None,
240 dead_code: InspectEvidenceSection::error(
241 InspectEvidenceScope::File,
242 "not run".to_string(),
243 ),
244 duplication: InspectEvidenceSection::error(
245 InspectEvidenceScope::ProjectFilteredToFile,
246 "not run".to_string(),
247 ),
248 complexity: InspectEvidenceSection::error(
249 InspectEvidenceScope::ProjectFilteredToFile,
250 "not run".to_string(),
251 ),
252 security: InspectEvidenceSection::error(
253 InspectEvidenceScope::File,
254 "not run".to_string(),
255 ),
256 impact_closure: InspectEvidenceSection::error(
257 InspectEvidenceScope::ProjectFilteredToFile,
258 "not run".to_string(),
259 ),
260 churn: None,
261 symbol_chain: None,
262 },
263 warnings: Vec::new(),
264 };
265
266 let value =
267 serialize_inspect_json_output(output, RootEnvelopeMode::Tagged, Some("run-inspect"))
268 .expect("inspect output should serialize");
269
270 assert_eq!(value["kind"], "inspect_target");
271 assert_eq!(
272 value["_meta"]["telemetry"]["analysis_run_id"],
273 "run-inspect"
274 );
275 assert!(value["evidence"].get("churn").is_none());
276 }
277
278 #[test]
279 fn inspect_churn_section_serializes_success_unavailable_and_error_states() {
280 let ok = InspectEvidenceSection::ok(
281 InspectEvidenceScope::ProjectFilteredToFile,
282 serde_json::json!({"file": "src/app.ts", "commits": 4}),
283 );
284 let unavailable = InspectEvidenceSection::unavailable(
285 InspectEvidenceScope::ProjectFilteredToFile,
286 "git repository unavailable".to_string(),
287 );
288 let error = InspectEvidenceSection::error(
289 InspectEvidenceScope::ProjectFilteredToFile,
290 "git log failed".to_string(),
291 );
292
293 assert_eq!(
294 serde_json::to_value(ok).expect("ok section should serialize")["status"],
295 "ok"
296 );
297 assert_eq!(
298 serde_json::to_value(unavailable).expect("unavailable section should serialize")["status"],
299 "unavailable"
300 );
301 assert_eq!(
302 serde_json::to_value(error).expect("error section should serialize")["status"],
303 "error"
304 );
305 }
306}