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#[allow(
801 dead_code,
802 reason = "schema-source-of-truth: see `ListBoundariesOutput`."
803)]
804pub struct BoundariesListing {
805 pub configured: bool,
806 pub zone_count: usize,
807 pub zones: Vec<BoundariesListZone>,
808 pub rule_count: usize,
809 pub rules: Vec<BoundariesListRule>,
810 pub logical_group_count: usize,
811 pub logical_groups: Vec<BoundariesListLogicalGroup>,
812}
813
814#[derive(Debug, Clone, Serialize)]
817#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
818#[allow(
819 dead_code,
820 reason = "schema-source-of-truth: see `ListBoundariesOutput`."
821)]
822pub struct BoundariesListZone {
823 pub name: String,
824 pub patterns: Vec<String>,
825 pub file_count: usize,
826}
827
828#[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 BoundariesListRule {
839 pub from: String,
840 pub allow: Vec<String>,
841}
842
843#[derive(Debug, Clone, Serialize)]
848#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
849#[allow(
850 dead_code,
851 reason = "schema-source-of-truth: see `ListBoundariesOutput`."
852)]
853pub struct BoundariesListLogicalGroup {
854 pub name: String,
855 pub children: Vec<String>,
856 pub auto_discover: Vec<String>,
857 pub status: fallow_config::LogicalGroupStatus,
858 pub source_zone_index: usize,
859 pub file_count: usize,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub authored_rule: Option<fallow_config::AuthoredRule>,
862 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub fallback_zone: Option<String>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub merged_from: Option<Vec<usize>>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
867 pub original_zone_root: Option<String>,
868 #[serde(default, skip_serializing_if = "Vec::is_empty")]
869 pub child_source_indices: Vec<usize>,
870}
871
872#[derive(Debug, Clone, Serialize)]
890#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
891#[cfg_attr(
892 feature = "schema",
893 schemars(title = "fallow --format json (typed root)")
894)]
895#[serde(tag = "kind")]
896#[allow(
897 dead_code,
898 reason = "some variants are schema-emit only, but runtime roots serialize through this enum where practical"
899)]
900pub enum FallowOutput {
901 #[serde(rename = "audit")]
904 Audit(AuditOutput),
905 #[serde(rename = "explain")]
908 Explain(ExplainOutput),
909 #[serde(rename = "review-envelope")]
912 ReviewEnvelope(ReviewEnvelopeOutput),
913 #[serde(rename = "review-reconcile")]
917 ReviewReconcile(ReviewReconcileOutput),
918 #[serde(rename = "coverage-setup")]
921 CoverageSetup(CoverageSetupOutput),
922 #[serde(rename = "coverage-analyze")]
926 CoverageAnalyze(CoverageAnalyzeOutput),
927 #[serde(rename = "list-boundaries")]
930 ListBoundaries(ListBoundariesOutput),
931 #[serde(rename = "health")]
933 Health(HealthOutput),
934 #[serde(rename = "dupes")]
938 Dupes(DupesOutput),
939 #[serde(rename = "dead-code-grouped")]
942 CheckGrouped(CheckGroupedOutput),
943 #[serde(rename = "impact")]
947 Impact(crate::impact::ImpactReport),
948 #[serde(rename = "security")]
952 Security(crate::security::SecurityOutput),
953 #[serde(rename = "dead-code")]
956 Check(CheckOutput),
957 #[serde(rename = "combined")]
961 Combined(CombinedOutput),
962}
963
964#[cfg(test)]
965mod tests {
966 use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
967
968 use super::*;
969
970 fn combined_output() -> CombinedOutput {
971 CombinedOutput {
972 schema_version: SchemaVersion(crate::report::SCHEMA_VERSION),
973 version: ToolVersion("test".to_string()),
974 elapsed_ms: ElapsedMs(0),
975 meta: None,
976 check: None,
977 dupes: None,
978 health: None,
979 }
980 }
981
982 #[test]
983 fn root_output_serializes_kind_by_default() {
984 let value = serialize_root_output_with_mode(
985 FallowOutput::Combined(combined_output()),
986 EnvelopeMode::Tagged,
987 )
988 .expect("combined root should serialize");
989
990 assert_eq!(value["kind"], serde_json::Value::String("combined".into()));
991 assert_eq!(value["schema_version"], crate::report::SCHEMA_VERSION);
992 }
993
994 #[test]
995 fn legacy_mode_removes_only_root_kind() {
996 let value = serialize_root_output_with_mode(
997 FallowOutput::Combined(combined_output()),
998 EnvelopeMode::Legacy,
999 )
1000 .expect("combined root should serialize");
1001
1002 assert!(value.get("kind").is_none());
1003
1004 let mut nested = serde_json::json!({
1005 "kind": "root",
1006 "action": {
1007 "kind": "suppress"
1008 }
1009 });
1010 remove_root_kind(&mut nested);
1011 assert!(nested.get("kind").is_none());
1012 assert_eq!(nested["action"]["kind"], "suppress");
1013 }
1014}