1use crate::root_envelopes::{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,
21 pub name: String,
23 pub summary: String,
25 pub rationale: String,
27 pub example: String,
29 pub how_to_fix: String,
31 pub docs: String,
33}
34
35pub fn serialize_explain_json_output(
41 output: ExplainOutput,
42 analysis_run_id: Option<&str>,
43) -> Result<serde_json::Value, serde_json::Error> {
44 let mut value = serialize_named_json_output(output, "explain")?;
45 attach_telemetry_meta(&mut value, analysis_run_id);
46 Ok(value)
47}
48
49#[derive(Debug, Clone, Serialize)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[cfg_attr(feature = "schema", schemars(title = "fallow inspect --format json"))]
53pub struct InspectOutput {
54 pub target: InspectTargetDescriptor,
56 pub identity: InspectIdentity,
58 pub evidence: InspectEvidence,
60 pub warnings: Vec<String>,
62 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
64 pub meta: Option<fallow_types::envelope::Meta>,
65}
66
67#[derive(Debug, Clone, Serialize)]
69#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum InspectTargetDescriptor {
72 File {
74 file: String,
76 },
77 Symbol {
79 file: String,
81 export_name: String,
83 },
84}
85
86#[derive(Debug, Clone, Serialize)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89#[serde(untagged)]
90pub enum InspectIdentity {
91 File(InspectFileIdentity),
93 Symbol(InspectSymbolIdentity),
95}
96
97#[derive(Debug, Clone, Serialize)]
100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
101pub struct InspectFileIdentity {
102 pub file: String,
104 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
106 pub is_reachable: Option<serde_json::Value>,
107 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
109 pub is_entry_point: Option<serde_json::Value>,
110 pub export_count: Option<usize>,
112 pub import_count: Option<usize>,
114 pub imported_by_count: Option<usize>,
116}
117
118#[derive(Debug, Clone, Serialize)]
121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
122pub struct InspectSymbolIdentity {
123 pub file: String,
125 pub export_name: String,
127 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
129 pub file_reachable: Option<serde_json::Value>,
130 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
132 pub is_entry_point: Option<serde_json::Value>,
133 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
135 pub is_used: Option<serde_json::Value>,
136 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
138 pub reason: Option<serde_json::Value>,
139}
140
141#[derive(Debug, Clone, Serialize)]
143#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
144pub struct InspectEvidence {
145 pub trace_file: InspectEvidenceSection,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub trace_export: Option<InspectEvidenceSection>,
150 pub dead_code: InspectEvidenceSection,
152 pub duplication: InspectEvidenceSection,
154 pub complexity: InspectEvidenceSection,
156 pub security: InspectEvidenceSection,
158 pub impact_closure: InspectEvidenceSection,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub churn: Option<InspectEvidenceSection>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub symbol_chain: Option<InspectEvidenceSection>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub semantic_trace: Option<InspectEvidenceSection>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub api_surface: Option<InspectEvidenceSection>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub symbol_impact: Option<InspectEvidenceSection>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub targeted_tests: Option<InspectEvidenceSection>,
187}
188
189#[derive(Debug, Clone, Serialize)]
192#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
193pub struct InspectEvidenceSection {
194 pub status: InspectSectionStatus,
196 pub scope: InspectEvidenceScope,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub message: Option<String>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub data: Option<serde_json::Value>,
204}
205
206impl InspectEvidenceSection {
207 #[must_use]
209 pub fn ok(scope: InspectEvidenceScope, data: serde_json::Value) -> Self {
210 Self {
211 status: InspectSectionStatus::Ok,
212 scope,
213 message: None,
214 data: Some(data),
215 }
216 }
217
218 #[must_use]
221 pub fn semantic(
222 scope: InspectEvidenceScope,
223 status: fallow_types::semantic::SemanticCompleteness,
224 data: serde_json::Value,
225 ) -> Self {
226 let status = match status {
227 fallow_types::semantic::SemanticCompleteness::Complete => InspectSectionStatus::Ok,
228 fallow_types::semantic::SemanticCompleteness::Partial => InspectSectionStatus::Partial,
229 fallow_types::semantic::SemanticCompleteness::Unavailable => {
230 InspectSectionStatus::Unavailable
231 }
232 };
233 Self {
234 status,
235 scope,
236 message: None,
237 data: Some(data),
238 }
239 }
240
241 #[must_use]
243 pub fn error(scope: InspectEvidenceScope, message: String) -> Self {
244 Self {
245 status: InspectSectionStatus::Error,
246 scope,
247 message: Some(message),
248 data: None,
249 }
250 }
251
252 #[must_use]
254 pub fn unavailable(scope: InspectEvidenceScope, message: String) -> Self {
255 Self {
256 status: InspectSectionStatus::Unavailable,
257 scope,
258 message: Some(message),
259 data: None,
260 }
261 }
262}
263
264pub fn serialize_inspect_json_output(
270 output: InspectOutput,
271 analysis_run_id: Option<&str>,
272) -> Result<serde_json::Value, serde_json::Error> {
273 let mut value = serialize_named_json_output(output, "inspect_target")?;
274 attach_telemetry_meta(&mut value, analysis_run_id);
275 Ok(value)
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
280#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
281#[serde(rename_all = "snake_case")]
282pub enum InspectSectionStatus {
283 Ok,
285 Partial,
287 Unavailable,
289 Error,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
295#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
296#[serde(rename_all = "snake_case")]
297pub enum InspectEvidenceScope {
298 Symbol,
300 File,
302 ProjectFilteredToFile,
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn explain_json_output_uses_output_owned_root_contract() {
312 let output = ExplainOutput {
313 id: "unused-export".to_string(),
314 name: "Unused export".to_string(),
315 summary: "summary".to_string(),
316 rationale: "rationale".to_string(),
317 example: "example".to_string(),
318 how_to_fix: "fix".to_string(),
319 docs: "https://example.test".to_string(),
320 };
321
322 let value = serialize_explain_json_output(output, Some("run-explain"))
323 .expect("explain output should serialize");
324
325 assert_eq!(value["kind"], "explain");
326 assert_eq!(
327 value["_meta"]["telemetry"]["analysis_run_id"],
328 "run-explain"
329 );
330 }
331
332 #[test]
333 fn inspect_json_output_uses_output_owned_root_contract() {
334 let output = InspectOutput {
335 target: InspectTargetDescriptor::File {
336 file: "src/app.ts".to_string(),
337 },
338 identity: InspectIdentity::File(InspectFileIdentity {
339 file: "src/app.ts".to_string(),
340 is_reachable: None,
341 is_entry_point: None,
342 export_count: Some(0),
343 import_count: Some(0),
344 imported_by_count: Some(0),
345 }),
346 evidence: InspectEvidence {
347 trace_file: InspectEvidenceSection::ok(
348 InspectEvidenceScope::File,
349 serde_json::json!({}),
350 ),
351 trace_export: None,
352 dead_code: InspectEvidenceSection::error(
353 InspectEvidenceScope::File,
354 "not run".to_string(),
355 ),
356 duplication: InspectEvidenceSection::error(
357 InspectEvidenceScope::ProjectFilteredToFile,
358 "not run".to_string(),
359 ),
360 complexity: InspectEvidenceSection::error(
361 InspectEvidenceScope::ProjectFilteredToFile,
362 "not run".to_string(),
363 ),
364 security: InspectEvidenceSection::error(
365 InspectEvidenceScope::File,
366 "not run".to_string(),
367 ),
368 impact_closure: InspectEvidenceSection::error(
369 InspectEvidenceScope::ProjectFilteredToFile,
370 "not run".to_string(),
371 ),
372 churn: None,
373 symbol_chain: None,
374 semantic_trace: None,
375 api_surface: None,
376 symbol_impact: None,
377 targeted_tests: None,
378 },
379 warnings: Vec::new(),
380 meta: Some(fallow_types::envelope::Meta {
381 type_aware: Some(fallow_types::envelope::TypeAwareMeta {
382 protocol_version: 7,
383 backend: "typescript-go".to_string(),
384 ..fallow_types::envelope::TypeAwareMeta::default()
385 }),
386 ..fallow_types::envelope::Meta::default()
387 }),
388 };
389
390 let value = serialize_inspect_json_output(output, Some("run-inspect"))
391 .expect("inspect output should serialize");
392
393 assert_eq!(value["kind"], "inspect_target");
394 assert_eq!(
395 value["_meta"]["telemetry"]["analysis_run_id"],
396 "run-inspect"
397 );
398 assert_eq!(value["_meta"]["type_aware"]["protocol_version"], 7);
399 assert_eq!(value["_meta"]["type_aware"]["backend"], "typescript-go");
400 assert!(value["evidence"].get("churn").is_none());
401 }
402
403 #[test]
404 fn inspect_churn_section_serializes_success_unavailable_and_error_states() {
405 let ok = InspectEvidenceSection::ok(
406 InspectEvidenceScope::ProjectFilteredToFile,
407 serde_json::json!({"file": "src/app.ts", "commits": 4}),
408 );
409 let unavailable = InspectEvidenceSection::unavailable(
410 InspectEvidenceScope::ProjectFilteredToFile,
411 "git repository unavailable".to_string(),
412 );
413 let error = InspectEvidenceSection::error(
414 InspectEvidenceScope::ProjectFilteredToFile,
415 "git log failed".to_string(),
416 );
417
418 assert_eq!(
419 serde_json::to_value(ok).expect("ok section should serialize")["status"],
420 "ok"
421 );
422 assert_eq!(
423 serde_json::to_value(unavailable).expect("unavailable section should serialize")["status"],
424 "unavailable"
425 );
426 assert_eq!(
427 serde_json::to_value(error).expect("error section should serialize")["status"],
428 "error"
429 );
430 }
431}