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,
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 mode: RootEnvelopeMode,
43 analysis_run_id: Option<&str>,
44) -> Result<serde_json::Value, serde_json::Error> {
45 let mut value = serialize_named_json_output(output, "explain", mode)?;
46 attach_telemetry_meta(&mut value, analysis_run_id);
47 Ok(value)
48}
49
50#[derive(Debug, Clone, Serialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[cfg_attr(feature = "schema", schemars(title = "fallow inspect --format json"))]
54pub struct InspectOutput {
55 pub target: InspectTargetDescriptor,
57 pub identity: InspectIdentity,
59 pub evidence: InspectEvidence,
61 pub warnings: Vec<String>,
63 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
65 pub meta: Option<fallow_types::envelope::Meta>,
66}
67
68#[derive(Debug, Clone, Serialize)]
70#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
71#[serde(tag = "type", rename_all = "snake_case")]
72pub enum InspectTargetDescriptor {
73 File {
75 file: String,
77 },
78 Symbol {
80 file: String,
82 export_name: String,
84 },
85}
86
87#[derive(Debug, Clone, Serialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(untagged)]
91pub enum InspectIdentity {
92 File(InspectFileIdentity),
94 Symbol(InspectSymbolIdentity),
96}
97
98#[derive(Debug, Clone, Serialize)]
101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
102pub struct InspectFileIdentity {
103 pub file: String,
105 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
107 pub is_reachable: Option<serde_json::Value>,
108 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
110 pub is_entry_point: Option<serde_json::Value>,
111 pub export_count: Option<usize>,
113 pub import_count: Option<usize>,
115 pub imported_by_count: Option<usize>,
117}
118
119#[derive(Debug, Clone, Serialize)]
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123pub struct InspectSymbolIdentity {
124 pub file: String,
126 pub export_name: String,
128 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
130 pub file_reachable: Option<serde_json::Value>,
131 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
133 pub is_entry_point: Option<serde_json::Value>,
134 #[cfg_attr(feature = "schema", schemars(with = "Option<bool>"))]
136 pub is_used: Option<serde_json::Value>,
137 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
139 pub reason: Option<serde_json::Value>,
140}
141
142#[derive(Debug, Clone, Serialize)]
144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
145pub struct InspectEvidence {
146 pub trace_file: InspectEvidenceSection,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub trace_export: Option<InspectEvidenceSection>,
151 pub dead_code: InspectEvidenceSection,
153 pub duplication: InspectEvidenceSection,
155 pub complexity: InspectEvidenceSection,
157 pub security: InspectEvidenceSection,
159 pub impact_closure: InspectEvidenceSection,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub churn: Option<InspectEvidenceSection>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub symbol_chain: Option<InspectEvidenceSection>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub semantic_trace: Option<InspectEvidenceSection>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub api_surface: Option<InspectEvidenceSection>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub symbol_impact: Option<InspectEvidenceSection>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub targeted_tests: Option<InspectEvidenceSection>,
188}
189
190#[derive(Debug, Clone, Serialize)]
193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
194pub struct InspectEvidenceSection {
195 pub status: InspectSectionStatus,
197 pub scope: InspectEvidenceScope,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub message: Option<String>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub data: Option<serde_json::Value>,
205}
206
207impl InspectEvidenceSection {
208 #[must_use]
210 pub fn ok(scope: InspectEvidenceScope, data: serde_json::Value) -> Self {
211 Self {
212 status: InspectSectionStatus::Ok,
213 scope,
214 message: None,
215 data: Some(data),
216 }
217 }
218
219 #[must_use]
222 pub fn semantic(
223 scope: InspectEvidenceScope,
224 status: fallow_types::semantic::SemanticCompleteness,
225 data: serde_json::Value,
226 ) -> Self {
227 let status = match status {
228 fallow_types::semantic::SemanticCompleteness::Complete => InspectSectionStatus::Ok,
229 fallow_types::semantic::SemanticCompleteness::Partial => InspectSectionStatus::Partial,
230 fallow_types::semantic::SemanticCompleteness::Unavailable => {
231 InspectSectionStatus::Unavailable
232 }
233 };
234 Self {
235 status,
236 scope,
237 message: None,
238 data: Some(data),
239 }
240 }
241
242 #[must_use]
244 pub fn error(scope: InspectEvidenceScope, message: String) -> Self {
245 Self {
246 status: InspectSectionStatus::Error,
247 scope,
248 message: Some(message),
249 data: None,
250 }
251 }
252
253 #[must_use]
255 pub fn unavailable(scope: InspectEvidenceScope, message: String) -> Self {
256 Self {
257 status: InspectSectionStatus::Unavailable,
258 scope,
259 message: Some(message),
260 data: None,
261 }
262 }
263}
264
265pub fn serialize_inspect_json_output(
271 output: InspectOutput,
272 mode: RootEnvelopeMode,
273 analysis_run_id: Option<&str>,
274) -> Result<serde_json::Value, serde_json::Error> {
275 let mut value = serialize_named_json_output(output, "inspect_target", mode)?;
276 attach_telemetry_meta(&mut value, analysis_run_id);
277 Ok(value)
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
282#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
283#[serde(rename_all = "snake_case")]
284pub enum InspectSectionStatus {
285 Ok,
287 Partial,
289 Unavailable,
291 Error,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
298#[serde(rename_all = "snake_case")]
299pub enum InspectEvidenceScope {
300 Symbol,
302 File,
304 ProjectFilteredToFile,
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn explain_json_output_uses_output_owned_root_contract() {
314 let output = ExplainOutput {
315 id: "unused-export".to_string(),
316 name: "Unused export".to_string(),
317 summary: "summary".to_string(),
318 rationale: "rationale".to_string(),
319 example: "example".to_string(),
320 how_to_fix: "fix".to_string(),
321 docs: "https://example.test".to_string(),
322 };
323
324 let value =
325 serialize_explain_json_output(output, RootEnvelopeMode::Tagged, Some("run-explain"))
326 .expect("explain output should serialize");
327
328 assert_eq!(value["kind"], "explain");
329 assert_eq!(
330 value["_meta"]["telemetry"]["analysis_run_id"],
331 "run-explain"
332 );
333 }
334
335 #[test]
336 fn inspect_json_output_uses_output_owned_root_contract() {
337 let output = InspectOutput {
338 target: InspectTargetDescriptor::File {
339 file: "src/app.ts".to_string(),
340 },
341 identity: InspectIdentity::File(InspectFileIdentity {
342 file: "src/app.ts".to_string(),
343 is_reachable: None,
344 is_entry_point: None,
345 export_count: Some(0),
346 import_count: Some(0),
347 imported_by_count: Some(0),
348 }),
349 evidence: InspectEvidence {
350 trace_file: InspectEvidenceSection::ok(
351 InspectEvidenceScope::File,
352 serde_json::json!({}),
353 ),
354 trace_export: None,
355 dead_code: InspectEvidenceSection::error(
356 InspectEvidenceScope::File,
357 "not run".to_string(),
358 ),
359 duplication: InspectEvidenceSection::error(
360 InspectEvidenceScope::ProjectFilteredToFile,
361 "not run".to_string(),
362 ),
363 complexity: InspectEvidenceSection::error(
364 InspectEvidenceScope::ProjectFilteredToFile,
365 "not run".to_string(),
366 ),
367 security: InspectEvidenceSection::error(
368 InspectEvidenceScope::File,
369 "not run".to_string(),
370 ),
371 impact_closure: InspectEvidenceSection::error(
372 InspectEvidenceScope::ProjectFilteredToFile,
373 "not run".to_string(),
374 ),
375 churn: None,
376 symbol_chain: None,
377 semantic_trace: None,
378 api_surface: None,
379 symbol_impact: None,
380 targeted_tests: None,
381 },
382 warnings: Vec::new(),
383 meta: Some(fallow_types::envelope::Meta {
384 type_aware: Some(fallow_types::envelope::TypeAwareMeta {
385 protocol_version: 7,
386 backend: "typescript-go".to_string(),
387 ..fallow_types::envelope::TypeAwareMeta::default()
388 }),
389 ..fallow_types::envelope::Meta::default()
390 }),
391 };
392
393 let value =
394 serialize_inspect_json_output(output, RootEnvelopeMode::Tagged, Some("run-inspect"))
395 .expect("inspect output should serialize");
396
397 assert_eq!(value["kind"], "inspect_target");
398 assert_eq!(
399 value["_meta"]["telemetry"]["analysis_run_id"],
400 "run-inspect"
401 );
402 assert_eq!(value["_meta"]["type_aware"]["protocol_version"], 7);
403 assert_eq!(value["_meta"]["type_aware"]["backend"], "typescript-go");
404 assert!(value["evidence"].get("churn").is_none());
405 }
406
407 #[test]
408 fn inspect_churn_section_serializes_success_unavailable_and_error_states() {
409 let ok = InspectEvidenceSection::ok(
410 InspectEvidenceScope::ProjectFilteredToFile,
411 serde_json::json!({"file": "src/app.ts", "commits": 4}),
412 );
413 let unavailable = InspectEvidenceSection::unavailable(
414 InspectEvidenceScope::ProjectFilteredToFile,
415 "git repository unavailable".to_string(),
416 );
417 let error = InspectEvidenceSection::error(
418 InspectEvidenceScope::ProjectFilteredToFile,
419 "git log failed".to_string(),
420 );
421
422 assert_eq!(
423 serde_json::to_value(ok).expect("ok section should serialize")["status"],
424 "ok"
425 );
426 assert_eq!(
427 serde_json::to_value(unavailable).expect("unavailable section should serialize")["status"],
428 "unavailable"
429 );
430 assert_eq!(
431 serde_json::to_value(error).expect("error section should serialize")["status"],
432 "error"
433 );
434 }
435}