1use std::sync::atomic::{AtomicBool, Ordering};
7
8use fallow_core::results::AnalysisResults;
9use fallow_types::envelope::{
10 BaselineDeltas, BaselineMatch, CheckSummary, ElapsedMs, EntryPoints, Meta, RegressionResult,
11 SchemaVersion, ToolVersion,
12};
13use serde::Serialize;
14
15use crate::audit::{AuditAttribution, AuditSummary, AuditVerdict};
16use crate::health_types::{HealthGroup, HealthReport, RuntimeCoverageReport};
17use crate::output_dupes::DupesReportPayload;
18use crate::report::dupes_grouping::DuplicationGroup;
19
20static LEGACY_ENVELOPE: AtomicBool = AtomicBool::new(false);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EnvelopeMode {
24 Tagged,
25 Legacy,
26}
27
28impl EnvelopeMode {
29 #[must_use]
30 pub fn current() -> Self {
31 if LEGACY_ENVELOPE.load(Ordering::Relaxed) {
32 Self::Legacy
33 } else {
34 Self::Tagged
35 }
36 }
37}
38
39pub fn set_legacy_envelope(enabled: bool) {
40 LEGACY_ENVELOPE.store(enabled, Ordering::Relaxed);
41}
42
43pub fn serialize_root_output(output: FallowOutput) -> Result<serde_json::Value, serde_json::Error> {
44 serialize_root_output_with_mode(output, EnvelopeMode::current())
45}
46
47pub fn serialize_root_output_with_mode(
48 output: FallowOutput,
49 mode: EnvelopeMode,
50) -> Result<serde_json::Value, serde_json::Error> {
51 let mut value = serde_json::to_value(output)?;
52 if mode == EnvelopeMode::Legacy {
53 remove_root_kind(&mut value);
54 }
55 Ok(value)
56}
57
58pub fn remove_root_kind(value: &mut serde_json::Value) {
62 if let serde_json::Value::Object(map) = value {
63 map.remove("kind");
64 }
65}
66
67pub fn apply_root_kind(value: &mut serde_json::Value, kind: &'static str) {
68 if EnvelopeMode::current() == EnvelopeMode::Tagged
69 && let serde_json::Value::Object(map) = value
70 {
71 map.insert(
72 "kind".to_string(),
73 serde_json::Value::String(kind.to_string()),
74 );
75 }
76}
77#[derive(Debug, Clone, Serialize)]
79#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
80#[cfg_attr(feature = "schema", schemars(title = "fallow coverage setup --json"))]
81pub struct CoverageSetupOutput {
82 pub schema_version: CoverageSetupSchemaVersion,
83 pub framework_detected: CoverageSetupFramework,
84 pub package_manager: Option<CoverageSetupPackageManager>,
85 pub runtime_targets: Vec<CoverageSetupRuntimeTarget>,
86 pub members: Vec<CoverageSetupMember>,
87 pub config_written: Option<serde_json::Value>,
88 pub commands: Vec<String>,
89 pub files_to_edit: Vec<CoverageSetupFileToEdit>,
90 pub snippets: Vec<CoverageSetupSnippet>,
91 pub dockerfile_snippet: Option<String>,
92 pub next_steps: Vec<String>,
93 pub warnings: Vec<String>,
94 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
95 pub meta: Option<serde_json::Value>,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
99#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
100pub enum CoverageSetupSchemaVersion {
101 #[serde(rename = "1")]
102 V1,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
106#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
107#[serde(rename_all = "snake_case")]
108pub enum CoverageSetupFramework {
109 #[serde(rename = "nextjs")]
110 NextJs,
111 #[serde(rename = "nestjs")]
112 NestJs,
113 Nuxt,
114 #[serde(rename = "sveltekit")]
115 SvelteKit,
116 Astro,
117 Remix,
118 Vite,
119 PlainNode,
120 Unknown,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
125#[serde(rename_all = "lowercase")]
126pub enum CoverageSetupPackageManager {
127 Npm,
128 Pnpm,
129 Yarn,
130 Bun,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135#[serde(rename_all = "lowercase")]
136pub enum CoverageSetupRuntimeTarget {
137 Node,
138 Browser,
139}
140
141#[derive(Debug, Clone, Serialize)]
142#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
143pub struct CoverageSetupMember {
144 pub name: String,
145 pub path: String,
146 pub framework_detected: CoverageSetupFramework,
147 pub package_manager: Option<CoverageSetupPackageManager>,
148 pub runtime_targets: Vec<CoverageSetupRuntimeTarget>,
149 pub files_to_edit: Vec<CoverageSetupFileToEdit>,
150 pub snippets: Vec<CoverageSetupSnippet>,
151 pub dockerfile_snippet: Option<String>,
152 pub warnings: Vec<String>,
153}
154
155#[derive(Debug, Clone, Serialize)]
156#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
157pub struct CoverageSetupFileToEdit {
158 pub path: String,
159 pub reason: String,
160}
161
162#[derive(Debug, Clone, Serialize)]
163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
164pub struct CoverageSetupSnippet {
165 pub label: String,
166 pub path: String,
167 pub content: String,
168}
169
170#[derive(Debug, Clone, Serialize)]
172#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
173#[cfg_attr(feature = "schema", schemars(title = "fallow audit --format json"))]
174#[allow(
175 dead_code,
176 reason = "schema-source-of-truth: audit.rs still builds the wire via serde_json::json!; this struct locks the schema shape via the drift gate. Migration is a follow-up to issue #384 items 3a/3b/3c."
177)]
178pub struct AuditOutput {
179 pub schema_version: SchemaVersion,
180 pub version: ToolVersion,
181 pub command: AuditCommand,
182 pub verdict: AuditVerdict,
183 pub changed_files_count: u32,
184 pub base_ref: String,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub head_sha: Option<String>,
187 pub elapsed_ms: ElapsedMs,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub base_snapshot_skipped: Option<bool>,
190 pub summary: AuditSummary,
191 pub attribution: AuditAttribution,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub dead_code: Option<CheckOutput>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub duplication: Option<DupesReportPayload>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub complexity: Option<HealthReport>,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
202#[serde(rename_all = "lowercase")]
203#[allow(dead_code, reason = "schema-source-of-truth: see `AuditOutput`.")]
204pub enum AuditCommand {
205 Audit,
206}
207
208#[derive(Debug, Clone, Serialize)]
210#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
211#[cfg_attr(
212 feature = "schema",
213 schemars(title = "fallow --format json (bare, combined)")
214)]
215pub struct CombinedOutput {
216 pub schema_version: SchemaVersion,
217 pub version: ToolVersion,
218 pub elapsed_ms: ElapsedMs,
219 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
220 pub meta: Option<CombinedMeta>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub check: Option<CheckOutput>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub dupes: Option<DupesReportPayload>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub health: Option<HealthReport>,
227}
228
229#[derive(Debug, Clone, Serialize)]
230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
231pub struct CombinedMeta {
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub check: Option<Meta>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub dupes: Option<Meta>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub health: Option<Meta>,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
241#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
242pub enum CoverageAnalyzeSchemaVersion {
243 #[serde(rename = "1")]
244 V1,
245}
246
247#[derive(Debug, Clone, Serialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[cfg_attr(
250 feature = "schema",
251 schemars(title = "fallow coverage analyze --format json")
252)]
253pub struct CoverageAnalyzeOutput {
254 pub schema_version: CoverageAnalyzeSchemaVersion,
255 pub version: ToolVersion,
256 pub elapsed_ms: ElapsedMs,
257 pub runtime_coverage: RuntimeCoverageReport,
258 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
259 pub meta: Option<Meta>,
260}
261
262#[derive(Debug, Clone, Serialize)]
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
264#[cfg_attr(feature = "schema", schemars(title = "fallow dupes --format json"))]
265pub struct DupesOutput {
266 pub schema_version: SchemaVersion,
267 pub version: ToolVersion,
268 pub elapsed_ms: ElapsedMs,
269 #[serde(flatten)]
270 pub report: DupesReportPayload,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub grouped_by: Option<GroupByMode>,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub total_issues: Option<usize>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub groups: Option<Vec<DuplicationGroup>>,
277 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
280 pub meta: Option<Meta>,
281 #[serde(default, skip_serializing_if = "Vec::is_empty")]
287 pub workspace_diagnostics: Vec<fallow_config::WorkspaceDiagnostic>,
288}
289
290#[derive(Debug, Clone, Serialize)]
300#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
301#[cfg_attr(feature = "schema", schemars(title = "fallow dead-code --format json"))]
302pub struct CheckOutput {
303 pub schema_version: SchemaVersion,
304 pub version: ToolVersion,
305 pub elapsed_ms: ElapsedMs,
306 pub total_issues: usize,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub entry_points: Option<EntryPoints>,
309 pub summary: CheckSummary,
310 #[serde(flatten)]
311 pub results: AnalysisResults,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub baseline_deltas: Option<BaselineDeltas>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub baseline: Option<BaselineMatch>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub regression: Option<RegressionResult>,
318 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
319 pub meta: Option<Meta>,
320 #[serde(default, skip_serializing_if = "Vec::is_empty")]
321 pub workspace_diagnostics: Vec<fallow_config::WorkspaceDiagnostic>,
322}
323
324#[derive(Debug, Clone, Serialize)]
331#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
332#[cfg_attr(
333 feature = "schema",
334 schemars(
335 title = "fallow dead-code --group-by <owner|directory|package|section> --format json"
336 )
337)]
338pub struct CheckGroupedOutput {
339 pub schema_version: SchemaVersion,
340 pub version: ToolVersion,
341 pub elapsed_ms: ElapsedMs,
342 pub grouped_by: GroupByMode,
343 pub total_issues: usize,
344 pub groups: Vec<CheckGroupedEntry>,
345 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
346 pub meta: Option<Meta>,
347}
348
349#[derive(Debug, Clone, Serialize)]
353#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
354pub struct CheckGroupedEntry {
355 pub key: String,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub owners: Option<Vec<String>>,
358 pub total_issues: usize,
359 #[serde(flatten)]
360 pub results: AnalysisResults,
361}
362
363#[derive(Debug, Clone, Serialize)]
375#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
376#[cfg_attr(feature = "schema", schemars(title = "fallow health --format json"))]
377pub struct HealthOutput {
378 pub schema_version: SchemaVersion,
379 pub version: ToolVersion,
380 pub elapsed_ms: ElapsedMs,
381 #[serde(flatten)]
382 pub report: HealthReport,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub grouped_by: Option<GroupByMode>,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub groups: Option<Vec<HealthGroup>>,
387 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
388 pub meta: Option<Meta>,
389 #[serde(default, skip_serializing_if = "Vec::is_empty")]
390 pub workspace_diagnostics: Vec<fallow_config::WorkspaceDiagnostic>,
391}
392
393#[derive(Debug, Clone, Serialize)]
400#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
401#[cfg_attr(
402 feature = "schema",
403 schemars(title = "fallow explain <issue-type> --format json")
404)]
405pub struct ExplainOutput {
406 pub id: String,
407 pub name: String,
408 pub summary: String,
409 pub rationale: String,
410 pub example: String,
411 pub how_to_fix: String,
412 pub docs: String,
413}
414
415#[derive(Debug, Clone, Serialize)]
419#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
420#[cfg_attr(
421 feature = "schema",
422 schemars(title = "fallow --format codeclimate / gitlab-codequality")
423)]
424#[serde(transparent)]
425#[allow(
426 dead_code,
427 reason = "schema-source-of-truth wrapper: runtime emits a `Vec<CodeClimateIssue>` directly via `codeclimate::issues_to_value`; this newtype exists so `schemars` can title and document the bare-array shape for the drift gate."
428)]
429pub struct CodeClimateOutput(pub Vec<CodeClimateIssue>);
430
431#[derive(Debug, Clone, Serialize)]
433#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
434pub struct CodeClimateIssue {
435 #[serde(rename = "type")]
436 pub kind: CodeClimateIssueKind,
437 pub check_name: String,
438 pub description: String,
439 pub categories: Vec<String>,
440 pub severity: CodeClimateSeverity,
441 pub fingerprint: String,
442 pub location: CodeClimateLocation,
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
447#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
448#[serde(rename_all = "lowercase")]
449pub enum CodeClimateIssueKind {
450 Issue,
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
457#[serde(rename_all = "lowercase")]
458pub enum CodeClimateSeverity {
459 #[allow(
464 dead_code,
465 reason = "schema-source-of-truth: documents the full CodeClimate severity spec; runtime never produces this variant today, but the schema needs it so consumers can validate against either fallow output or a third-party CodeClimate emitter without spec divergence."
466 )]
467 Info,
468 Minor,
470 Major,
472 Critical,
474 #[allow(
477 dead_code,
478 reason = "schema-source-of-truth: documents the full CodeClimate severity spec; runtime never produces this variant today, but the schema needs it so consumers can validate against either fallow output or a third-party CodeClimate emitter without spec divergence."
479 )]
480 Blocker,
481}
482
483#[derive(Debug, Clone, Serialize)]
485#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
486pub struct CodeClimateLocation {
487 pub path: String,
489 pub lines: CodeClimateLines,
492}
493
494#[derive(Debug, Clone, Copy, Serialize)]
496#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
497pub struct CodeClimateLines {
498 pub begin: u32,
500}
501
502#[derive(Debug, Clone, Serialize)]
504#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
505#[cfg_attr(
506 feature = "schema",
507 schemars(title = "fallow --format review-github / review-gitlab")
508)]
509pub struct ReviewEnvelopeOutput {
510 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub event: Option<ReviewEnvelopeEvent>,
512 pub body: String,
513 #[serde(default = "ReviewEnvelopeSummary::empty_default")]
514 pub summary: ReviewEnvelopeSummary,
515 pub comments: Vec<ReviewComment>,
516 #[serde(default = "default_marker_regex")]
517 pub marker_regex: String,
518 #[serde(default = "default_marker_regex_flags")]
519 pub marker_regex_flags: String,
520 pub meta: ReviewEnvelopeMeta,
521}
522
523#[must_use]
525pub fn default_marker_regex() -> String {
526 MARKER_REGEX_V2.to_owned()
527}
528
529#[must_use]
531pub fn default_marker_regex_flags() -> String {
532 MARKER_REGEX_FLAGS_V2.to_owned()
533}
534
535pub const MARKER_REGEX_V2: &str =
537 r"^<!-- fallow-fingerprint:v2: ((?:[a-z]+:)?[0-9a-f]{16}) -->\s*$";
538
539pub const MARKER_REGEX_FLAGS_V2: &str = "m";
541
542#[derive(Debug, Clone, Serialize, Default)]
544#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
545pub struct ReviewEnvelopeSummary {
546 pub body: String,
547 pub fingerprint: String,
548}
549
550impl ReviewEnvelopeSummary {
551 #[must_use]
553 #[allow(
554 dead_code,
555 reason = "referenced via serde default = \"...\" attr; no direct callsite until Deserialize is derived"
556 )]
557 pub fn empty_default() -> Self {
558 Self::default()
559 }
560}
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
564#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
565pub enum ReviewEnvelopeEvent {
566 #[serde(rename = "COMMENT")]
567 Comment,
568}
569
570#[derive(Debug, Clone, Serialize)]
575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
576#[serde(untagged)]
577pub enum ReviewComment {
578 GitHub(GitHubReviewComment),
579 GitLab(GitLabReviewComment),
580}
581
582#[derive(Debug, Clone, Serialize)]
584#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
585pub struct GitHubReviewComment {
586 pub path: String,
587 pub line: u32,
588 pub side: GitHubReviewSide,
589 pub body: String,
590 pub fingerprint: String,
591 #[serde(default, skip_serializing_if = "is_false")]
592 pub truncated: bool,
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
597#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
598pub enum GitHubReviewSide {
599 #[serde(rename = "RIGHT")]
600 Right,
601}
602
603#[derive(Debug, Clone, Serialize)]
605#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
606pub struct GitLabReviewComment {
607 pub body: String,
608 pub position: GitLabReviewPosition,
609 pub fingerprint: String,
610 #[serde(default, skip_serializing_if = "is_false")]
611 pub truncated: bool,
612}
613
614#[must_use]
622#[allow(
623 clippy::trivially_copy_pass_by_ref,
624 reason = "serde's skip_serializing_if requires fn(&T) -> bool"
625)]
626pub fn is_false(value: &bool) -> bool {
627 !*value
628}
629
630#[derive(Debug, Clone, Serialize)]
633#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
634pub struct GitLabReviewPosition {
635 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub base_sha: Option<String>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub start_sha: Option<String>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub head_sha: Option<String>,
641 pub position_type: GitLabReviewPositionType,
642 pub old_path: String,
643 pub new_path: String,
644 pub new_line: u32,
645}
646
647#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
649#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
650#[serde(rename_all = "lowercase")]
651pub enum GitLabReviewPositionType {
652 Text,
653}
654
655#[derive(Debug, Clone, Serialize)]
657#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
658pub struct ReviewEnvelopeMeta {
659 pub schema: ReviewEnvelopeSchema,
660 pub provider: ReviewProvider,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub check_conclusion: Option<ReviewCheckConclusion>,
663}
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
667#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
668pub enum ReviewEnvelopeSchema {
669 #[serde(rename = "fallow-review-envelope/v1")]
675 #[allow(
676 dead_code,
677 reason = "kept for forward-compat with v1 historical inputs once Deserialize is derived"
678 )]
679 V1,
680 #[serde(rename = "fallow-review-envelope/v2")]
694 V2,
695}
696
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
699#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
700#[serde(rename_all = "lowercase")]
701pub enum ReviewProvider {
702 Github,
704 Gitlab,
706}
707
708#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
711#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
712#[serde(rename_all = "lowercase")]
713pub enum ReviewCheckConclusion {
714 Success,
716 Neutral,
718 Failure,
720}
721
722#[derive(Debug, Clone, Serialize)]
726#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
727#[cfg_attr(
728 feature = "schema",
729 schemars(title = "fallow ci reconcile-review --format json")
730)]
731pub struct ReviewReconcileOutput {
732 pub schema: ReviewReconcileSchema,
733 pub provider: ReviewProvider,
734 pub target: Option<String>,
735 pub dry_run: bool,
736 pub comments: u32,
737 pub current_fingerprints: u32,
738 pub existing_fingerprints: u32,
739 pub new_fingerprints: u32,
740 pub stale_fingerprints: u32,
741 pub new: Vec<String>,
742 pub stale: Vec<String>,
743 pub provider_warning: Option<String>,
744 pub resolution_comments_posted: u32,
745 pub threads_resolved: u32,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub apply_hint: Option<String>,
748 pub apply_errors: Vec<String>,
749 #[serde(default, skip_serializing_if = "Vec::is_empty")]
750 pub failed_fingerprints: Vec<String>,
751 #[serde(default, skip_serializing_if = "Vec::is_empty")]
752 pub unapplied_fingerprints: Vec<String>,
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
757#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
758pub enum ReviewReconcileSchema {
759 #[serde(rename = "fallow-review-reconcile/v1")]
761 V1,
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
770#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
771#[serde(rename_all = "lowercase")]
772pub enum GroupByMode {
773 Owner,
774 Directory,
775 Package,
776 Section,
777}
778#[derive(Debug, Clone, Serialize)]
784#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
785#[cfg_attr(
786 feature = "schema",
787 schemars(title = "fallow list --boundaries --format json")
788)]
789#[allow(
790 dead_code,
791 reason = "schema-source-of-truth: list.rs still builds the wire via serde_json::json!; this struct and its sub-types lock the schema shape via the drift gate. Migration is a follow-up to issue #384 items 3a/3b/3c."
792)]
793pub struct ListBoundariesOutput {
794 pub boundaries: BoundariesListing,
795}
796
797#[derive(Debug, Clone, Serialize)]
799#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
800#[cfg_attr(
801 feature = "schema",
802 schemars(title = "fallow workspaces --format json")
803)]
804pub struct WorkspacesOutput {
805 pub workspace_count: usize,
807 pub workspaces: Vec<WorkspaceInfo>,
810 pub workspace_diagnostics: Vec<fallow_config::WorkspaceDiagnostic>,
814}
815
816#[derive(Debug, Clone, Serialize)]
818#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
819pub struct WorkspaceInfo {
820 pub name: String,
823 pub path: String,
826 pub is_internal_dependency: bool,
829}
830
831#[derive(Debug, Clone, Serialize)]
833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
834#[allow(
835 dead_code,
836 reason = "schema-source-of-truth: see `ListBoundariesOutput`."
837)]
838pub struct BoundariesListing {
839 pub configured: bool,
840 pub zone_count: usize,
841 pub zones: Vec<BoundariesListZone>,
842 pub rule_count: usize,
843 pub rules: Vec<BoundariesListRule>,
844 pub logical_group_count: usize,
845 pub logical_groups: Vec<BoundariesListLogicalGroup>,
846}
847
848#[derive(Debug, Clone, Serialize)]
851#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
852#[allow(
853 dead_code,
854 reason = "schema-source-of-truth: see `ListBoundariesOutput`."
855)]
856pub struct BoundariesListZone {
857 pub name: String,
858 pub patterns: Vec<String>,
859 pub file_count: usize,
860}
861
862#[derive(Debug, Clone, Serialize)]
867#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
868#[allow(
869 dead_code,
870 reason = "schema-source-of-truth: see `ListBoundariesOutput`."
871)]
872pub struct BoundariesListRule {
873 pub from: String,
874 pub allow: Vec<String>,
875}
876
877#[derive(Debug, Clone, Serialize)]
882#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
883#[allow(
884 dead_code,
885 reason = "schema-source-of-truth: see `ListBoundariesOutput`."
886)]
887pub struct BoundariesListLogicalGroup {
888 pub name: String,
889 pub children: Vec<String>,
890 pub auto_discover: Vec<String>,
891 pub status: fallow_config::LogicalGroupStatus,
892 pub source_zone_index: usize,
893 pub file_count: usize,
894 #[serde(default, skip_serializing_if = "Option::is_none")]
895 pub authored_rule: Option<fallow_config::AuthoredRule>,
896 #[serde(default, skip_serializing_if = "Option::is_none")]
897 pub fallback_zone: Option<String>,
898 #[serde(default, skip_serializing_if = "Option::is_none")]
899 pub merged_from: Option<Vec<usize>>,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub original_zone_root: Option<String>,
902 #[serde(default, skip_serializing_if = "Vec::is_empty")]
903 pub child_source_indices: Vec<usize>,
904}
905
906#[derive(Debug, Clone, Serialize)]
924#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
925#[cfg_attr(
926 feature = "schema",
927 schemars(title = "fallow --format json (typed root)")
928)]
929#[serde(tag = "kind")]
930#[allow(
931 dead_code,
932 reason = "some variants are schema-emit only, but runtime roots serialize through this enum where practical"
933)]
934pub enum FallowOutput {
935 #[serde(rename = "audit")]
938 Audit(AuditOutput),
939 #[serde(rename = "explain")]
942 Explain(ExplainOutput),
943 #[serde(rename = "review-envelope")]
946 ReviewEnvelope(ReviewEnvelopeOutput),
947 #[serde(rename = "review-reconcile")]
951 ReviewReconcile(ReviewReconcileOutput),
952 #[serde(rename = "coverage-setup")]
955 CoverageSetup(CoverageSetupOutput),
956 #[serde(rename = "coverage-analyze")]
960 CoverageAnalyze(CoverageAnalyzeOutput),
961 #[serde(rename = "list-boundaries")]
964 ListBoundaries(ListBoundariesOutput),
965 #[serde(rename = "list-workspaces")]
968 Workspaces(WorkspacesOutput),
969 #[serde(rename = "health")]
971 Health(HealthOutput),
972 #[serde(rename = "dupes")]
976 Dupes(DupesOutput),
977 #[serde(rename = "dead-code-grouped")]
980 CheckGrouped(CheckGroupedOutput),
981 #[serde(rename = "impact")]
985 Impact(crate::impact::ImpactReport),
986 #[serde(rename = "security")]
991 Security(crate::security::SecurityOutput),
992 #[serde(rename = "dead-code")]
995 Check(CheckOutput),
996 #[serde(rename = "combined")]
1000 Combined(CombinedOutput),
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005 use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
1006
1007 use super::*;
1008
1009 fn combined_output() -> CombinedOutput {
1010 CombinedOutput {
1011 schema_version: SchemaVersion(crate::report::SCHEMA_VERSION),
1012 version: ToolVersion("test".to_string()),
1013 elapsed_ms: ElapsedMs(0),
1014 meta: None,
1015 check: None,
1016 dupes: None,
1017 health: None,
1018 }
1019 }
1020
1021 #[test]
1022 fn root_output_serializes_kind_by_default() {
1023 let value = serialize_root_output_with_mode(
1024 FallowOutput::Combined(combined_output()),
1025 EnvelopeMode::Tagged,
1026 )
1027 .expect("combined root should serialize");
1028
1029 assert_eq!(value["kind"], serde_json::Value::String("combined".into()));
1030 assert_eq!(value["schema_version"], crate::report::SCHEMA_VERSION);
1031 }
1032
1033 #[test]
1034 fn legacy_mode_removes_only_root_kind() {
1035 let value = serialize_root_output_with_mode(
1036 FallowOutput::Combined(combined_output()),
1037 EnvelopeMode::Legacy,
1038 )
1039 .expect("combined root should serialize");
1040
1041 assert!(value.get("kind").is_none());
1042
1043 let mut nested = serde_json::json!({
1044 "kind": "root",
1045 "action": {
1046 "kind": "suppress"
1047 }
1048 });
1049 remove_root_kind(&mut nested);
1050 assert!(nested.get("kind").is_none());
1051 assert_eq!(nested["action"]["kind"], "suppress");
1052 }
1053}