fallow_types/envelope.rs
1//! Typed envelope and utility-shape structs for the JSON output contract.
2//!
3//! Today the JSON serialization layer (`crates/cli/src/report/json.rs`) builds
4//! its envelopes (`CheckOutput`, `HealthOutput`, ...) via `serde_json::json!`
5//! macros and ad-hoc map merging. The types in this module are the schema-side
6//! counterpart of those envelopes plus a small set of utility shapes
7//! (`SchemaVersion`, `Meta`, `BaselineDeltas`, ...) that the envelopes
8//! reference.
9//!
10//! Gated on the `schema` cargo feature so consumers that do not need the
11//! `schemars::JsonSchema` derive (every crate except `fallow-cli` with
12//! `--features schema-emit`) skip the schemars compile cost.
13
14use std::collections::BTreeMap;
15
16use serde::Serialize;
17
18/// Schema version for this output format (independent of tool version). Bump
19/// policy: ADDITIVE changes (new optional top-level fields, new optional struct
20/// fields, new array entries, new MCP tools, new CLI flags that map to new
21/// optional fields) do NOT bump the version; consumers receive new fields
22/// without breaking. BREAKING changes (renamed fields, removed fields, type
23/// changes, enum-variant removals, semantic changes to existing fields) DO
24/// bump. To detect newly-added fields without a bump, check field presence via
25/// JSON-key existence rather than gating on the version. v4 was introduced
26/// alongside fallow-cov-protocol 0.2 (per-finding verdict, stable IDs, evidence
27/// block, renamed summary fields); v5 introduced health_score formula_version 2
28/// with scale-invariant scoring semantics; v6 widened `AddToConfigAction.value`
29/// from a scalar string to `oneOf: [string, array]` so the new `ignoreExports`
30/// action can carry a paste-ready array of `{ file, exports }` rule objects
31/// (the legacy `ignoreDependencies` etc. variants still emit strings, so
32/// consumers that switch on `config_key` keep working unchanged). The
33/// runtime-coverage block is extended additively as the protocol evolves
34/// (currently 0.3, which adds an optional capture_quality summary field). Other
35/// additive examples: dupes --group-by adds optional grouped_by, total_issues,
36/// groups fields without bumping.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
38#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
39#[serde(transparent)]
40pub struct SchemaVersion(pub u32);
41
42/// Fallow CLI version that produced this envelope. Renders to the JSON wire as
43/// a bare string (e.g. `"2.74.0"`).
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46#[serde(transparent)]
47pub struct ToolVersion(pub String);
48
49/// Analysis duration in milliseconds. Renders to the JSON wire as a bare
50/// integer.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[serde(transparent)]
54pub struct ElapsedMs(pub u64);
55
56/// Audit-mode marker emitted on each finding when `fallow audit --format json`
57/// runs with a base ref. `true` means the finding's structural key was not
58/// present at the base ref (introduced by the current changeset); `false`
59/// means it was inherited.
60///
61/// Outside of audit sub-results the field is omitted, so call sites typically
62/// hold `Option<AuditIntroduced>`. Renders to the JSON wire as a bare boolean.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
64#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
65#[serde(transparent)]
66pub struct AuditIntroduced(pub bool);
67
68/// Entry-point detection summary embedded in `CheckOutput` and the combined
69/// envelope.
70#[derive(Debug, Clone, Default, Serialize)]
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72pub struct EntryPoints {
73 /// Total number of detected entry points.
74 pub total: usize,
75 /// Breakdown of entry points by detection source (e.g., `"package.json"`,
76 /// `"next.js"`, `"config entry"`). Underscored keys so dashboards can
77 /// drill into individual sources.
78 pub sources: BTreeMap<String, usize>,
79}
80
81/// Per-category issue counts for dead-code analysis. Always present in
82/// `CheckOutput`; when `--summary` is used the individual issue arrays are
83/// omitted but this object stays populated.
84#[derive(Debug, Clone, Default, Serialize)]
85#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
86pub struct CheckSummary {
87 /// Total number of issues across all categories.
88 pub total_issues: usize,
89 /// Unused source files.
90 pub unused_files: usize,
91 /// Unused value exports.
92 pub unused_exports: usize,
93 /// Unused type exports.
94 pub unused_types: usize,
95 /// Public exports whose signature references same-file private types.
96 pub private_type_leaks: usize,
97 /// Combined count of unused entries across `dependencies`,
98 /// `devDependencies`, and `optionalDependencies`. The per-section
99 /// breakdown lives in the individual issue arrays on `CheckOutput`.
100 pub unused_dependencies: usize,
101 /// Unused enum members.
102 pub unused_enum_members: usize,
103 /// Unused class members.
104 pub unused_class_members: usize,
105 /// Imports that could not be resolved against the project's module graph.
106 pub unresolved_imports: usize,
107 /// Dependencies imported but absent from `package.json`.
108 pub unlisted_dependencies: usize,
109 /// Same-named exports declared in more than one module.
110 pub duplicate_exports: usize,
111 /// Production dependencies only used via type-only imports (could be
112 /// devDependencies). Only populated in production mode.
113 pub type_only_dependencies: usize,
114 /// Production dependencies only imported by test files (could be
115 /// devDependencies).
116 pub test_only_dependencies: usize,
117 /// Cycles detected in the import graph.
118 pub circular_dependencies: usize,
119 /// Cycles or self-loops in the re-export edge subgraph (barrel files
120 /// re-exporting from each other in a loop).
121 #[serde(default)]
122 pub re_export_cycles: usize,
123 /// Imports that cross architecture boundary rules.
124 pub boundary_violations: usize,
125 /// Suppression comments that no longer match a finding.
126 pub stale_suppressions: usize,
127 /// Unused pnpm-workspace catalog entries.
128 pub unused_catalog_entries: usize,
129 /// Empty named catalog groups.
130 pub empty_catalog_groups: usize,
131 /// Workspace package.json catalog references the workspace catalogs
132 /// do not declare.
133 pub unresolved_catalog_references: usize,
134 /// Pnpm `overrides:` entries whose target package is not declared by any
135 /// workspace package and not present in the lockfile.
136 pub unused_dependency_overrides: usize,
137 /// Pnpm `overrides:` entries whose key or value cannot be parsed.
138 pub misconfigured_dependency_overrides: usize,
139}
140
141/// Per-category delta comparison against a saved baseline. Only present in
142/// `CheckOutput` when `--baseline` is used.
143#[derive(Debug, Clone, Default, Serialize)]
144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
145pub struct BaselineDeltas {
146 /// Net change in total issues vs baseline (positive = more issues).
147 pub total_delta: i64,
148 /// Per-category breakdown of current, baseline, and delta counts.
149 pub per_category: BTreeMap<String, BaselineCategoryDelta>,
150}
151
152/// Single-category baseline delta entry inside [`BaselineDeltas::per_category`].
153#[derive(Debug, Clone, Copy, Default, Serialize)]
154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
155pub struct BaselineCategoryDelta {
156 /// Current issue count for this category.
157 pub current: usize,
158 /// Baseline issue count for this category.
159 pub baseline: usize,
160 /// Change from baseline (current - baseline).
161 pub delta: i64,
162}
163
164/// Baseline match statistics. Shows how many baseline entries existed and how
165/// many matched current issues. Useful for detecting stale baselines
166/// programmatically. Only present in `CheckOutput` when `--baseline` is used.
167#[derive(Debug, Clone, Copy, Default, Serialize)]
168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
169pub struct BaselineMatch {
170 /// Total number of entries in the loaded baseline file.
171 pub entries: usize,
172 /// Number of baseline entries that matched current issues and were
173 /// filtered.
174 pub matched: usize,
175}
176
177/// Result of regression detection (`--fail-on-regression`). Compares current
178/// issue counts against a baseline from config or an explicit file.
179#[derive(Debug, Clone, Serialize)]
180#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
181pub struct RegressionResult {
182 /// Outcome of the regression check.
183 pub status: RegressionStatus,
184 /// Baseline total before the change. Absent when status is `skipped`.
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub baseline_total: Option<i64>,
187 /// Current total after the change. Absent when status is `skipped`.
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub current_total: Option<i64>,
190 /// Difference current - baseline. Absent when status is `skipped`.
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub delta: Option<i64>,
193 /// Configured tolerance, interpreted per [`RegressionToleranceKind`].
194 /// Absent when status is `skipped`.
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub tolerance: Option<f64>,
197 /// Interpretation of the tolerance value.
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub tolerance_kind: Option<RegressionToleranceKind>,
200 /// Whether the regression exceeded the tolerance.
201 pub exceeded: bool,
202 /// Only present when status is `skipped`.
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub reason: Option<String>,
205}
206
207/// Status of a regression-check pass.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
210#[serde(rename_all = "lowercase")]
211pub enum RegressionStatus {
212 /// Issue count within tolerance.
213 Pass,
214 /// Issue count exceeded tolerance.
215 Exceeded,
216 /// Regression check did not run (missing baseline, etc.).
217 Skipped,
218}
219
220/// Interpretation of [`RegressionResult::tolerance`].
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
223#[serde(rename_all = "lowercase")]
224pub enum RegressionToleranceKind {
225 /// Tolerance is interpreted as an absolute issue-count delta.
226 Absolute,
227 /// Tolerance is interpreted as a percentage of the baseline total.
228 Percentage,
229}
230
231/// Metric and rule definitions emitted under `_meta` when `--explain` is
232/// passed (always present in MCP responses). Helps AI agents and CI systems
233/// interpret metric values without re-reading the docs site.
234#[derive(Debug, Clone, Default, Serialize)]
235#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236pub struct Meta {
237 /// URL to the documentation page for this command.
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub docs: Option<String>,
240 /// Per-metric definitions: name, description, range, interpretation.
241 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
242 pub metrics: BTreeMap<String, MetaMetric>,
243 /// Per-rule definitions for check command output.
244 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
245 pub rules: BTreeMap<String, MetaRule>,
246}
247
248/// Single-metric definition inside [`Meta::metrics`].
249#[derive(Debug, Clone, Default, Serialize)]
250#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
251pub struct MetaMetric {
252 /// Human-readable metric name.
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub name: Option<String>,
255 /// What this metric measures and how it is computed.
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub description: Option<String>,
258 /// Valid value range (e.g., `"[0, 100]"`).
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub range: Option<String>,
261 /// How to read the value (e.g., `"lower is better"`).
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub interpretation: Option<String>,
264}
265
266/// Single-rule definition inside [`Meta::rules`].
267#[derive(Debug, Clone, Default, Serialize)]
268#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
269pub struct MetaRule {
270 /// Human-readable rule name.
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub name: Option<String>,
273 /// What this rule detects.
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub description: Option<String>,
276 /// URL to the rule documentation.
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub docs: Option<String>,
279}