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")]
110 pub symbol_chain: Option<InspectEvidenceSection>,
111}
112
113#[derive(Debug, Clone, Serialize)]
114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
115pub struct InspectEvidenceSection {
116 pub status: InspectSectionStatus,
117 pub scope: InspectEvidenceScope,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub message: Option<String>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub data: Option<serde_json::Value>,
122}
123
124impl InspectEvidenceSection {
125 #[must_use]
126 pub fn ok(scope: InspectEvidenceScope, data: serde_json::Value) -> Self {
127 Self {
128 status: InspectSectionStatus::Ok,
129 scope,
130 message: None,
131 data: Some(data),
132 }
133 }
134
135 #[must_use]
136 pub fn error(scope: InspectEvidenceScope, message: String) -> Self {
137 Self {
138 status: InspectSectionStatus::Error,
139 scope,
140 message: Some(message),
141 data: None,
142 }
143 }
144}
145
146pub fn serialize_inspect_json_output(
152 output: InspectOutput,
153 mode: RootEnvelopeMode,
154 analysis_run_id: Option<&str>,
155) -> Result<serde_json::Value, serde_json::Error> {
156 let mut value = serialize_named_json_output(output, "inspect_target", mode)?;
157 attach_telemetry_meta(&mut value, analysis_run_id);
158 Ok(value)
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
162#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
163#[serde(rename_all = "snake_case")]
164pub enum InspectSectionStatus {
165 Ok,
166 Error,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
170#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
171#[serde(rename_all = "snake_case")]
172pub enum InspectEvidenceScope {
173 Symbol,
174 File,
175 ProjectFilteredToFile,
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn explain_json_output_uses_output_owned_root_contract() {
184 let output = ExplainOutput {
185 id: "unused-export".to_string(),
186 name: "Unused export".to_string(),
187 summary: "summary".to_string(),
188 rationale: "rationale".to_string(),
189 example: "example".to_string(),
190 how_to_fix: "fix".to_string(),
191 docs: "https://example.test".to_string(),
192 };
193
194 let value =
195 serialize_explain_json_output(output, RootEnvelopeMode::Tagged, Some("run-explain"))
196 .expect("explain output should serialize");
197
198 assert_eq!(value["kind"], "explain");
199 assert_eq!(
200 value["_meta"]["telemetry"]["analysis_run_id"],
201 "run-explain"
202 );
203 }
204
205 #[test]
206 fn inspect_json_output_uses_output_owned_root_contract() {
207 let output = InspectOutput {
208 target: InspectTargetDescriptor::File {
209 file: "src/app.ts".to_string(),
210 },
211 identity: InspectIdentity::File(InspectFileIdentity {
212 file: "src/app.ts".to_string(),
213 is_reachable: None,
214 is_entry_point: None,
215 export_count: Some(0),
216 import_count: Some(0),
217 imported_by_count: Some(0),
218 }),
219 evidence: InspectEvidence {
220 trace_file: InspectEvidenceSection::ok(
221 InspectEvidenceScope::File,
222 serde_json::json!({}),
223 ),
224 trace_export: None,
225 dead_code: InspectEvidenceSection::error(
226 InspectEvidenceScope::File,
227 "not run".to_string(),
228 ),
229 duplication: InspectEvidenceSection::error(
230 InspectEvidenceScope::ProjectFilteredToFile,
231 "not run".to_string(),
232 ),
233 complexity: InspectEvidenceSection::error(
234 InspectEvidenceScope::ProjectFilteredToFile,
235 "not run".to_string(),
236 ),
237 security: InspectEvidenceSection::error(
238 InspectEvidenceScope::File,
239 "not run".to_string(),
240 ),
241 impact_closure: InspectEvidenceSection::error(
242 InspectEvidenceScope::ProjectFilteredToFile,
243 "not run".to_string(),
244 ),
245 symbol_chain: None,
246 },
247 warnings: Vec::new(),
248 };
249
250 let value =
251 serialize_inspect_json_output(output, RootEnvelopeMode::Tagged, Some("run-inspect"))
252 .expect("inspect output should serialize");
253
254 assert_eq!(value["kind"], "inspect_target");
255 assert_eq!(
256 value["_meta"]["telemetry"]["analysis_run_id"],
257 "run-inspect"
258 );
259 }
260}