Skip to main content

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    /// Unused store members.
106    #[serde(default)]
107    pub unused_store_members: usize,
108    /// Vue/Svelte injects whose key is provided nowhere in the project.
109    #[serde(default)]
110    pub unprovided_injects: usize,
111    /// Vue/Svelte components reachable but rendered nowhere in the project.
112    #[serde(default)]
113    pub unrendered_components: usize,
114    /// Vue, Svelte, or React props referenced nowhere inside their own component.
115    #[serde(default)]
116    pub unused_component_props: usize,
117    /// Vue `<script setup>` emits emitted nowhere inside their own SFC.
118    #[serde(default)]
119    pub unused_component_emits: usize,
120    /// Angular `@Input()` bindings referenced nowhere inside their own component.
121    #[serde(default)]
122    pub unused_component_inputs: usize,
123    /// Angular `@Output()` bindings emitted nowhere inside their own component.
124    #[serde(default)]
125    pub unused_component_outputs: usize,
126    /// Svelte components dispatching a custom event via `createEventDispatcher`
127    /// whose name is listened to nowhere in the project.
128    #[serde(default)]
129    pub unused_svelte_events: usize,
130    /// Next.js Server Actions (exports of `"use server"` files) referenced by no
131    /// code in the project.
132    #[serde(default)]
133    pub unused_server_actions: usize,
134    /// SvelteKit `load()` return-object keys read by no consumer.
135    #[serde(default)]
136    pub unused_load_data_keys: usize,
137    /// Imports that could not be resolved against the project's module graph.
138    pub unresolved_imports: usize,
139    /// Dependencies imported but absent from `package.json`.
140    pub unlisted_dependencies: usize,
141    /// Same-named exports declared in more than one module.
142    pub duplicate_exports: usize,
143    /// Production dependencies only used via type-only imports (could be
144    /// devDependencies). Only populated in production mode.
145    pub type_only_dependencies: usize,
146    /// Production dependencies only imported by test files (could be
147    /// devDependencies).
148    pub test_only_dependencies: usize,
149    /// devDependencies imported by production source code with a runtime/value
150    /// import (should be promoted to dependencies).
151    pub dev_dependencies_in_production: usize,
152    /// Cycles detected in the import graph.
153    pub circular_dependencies: usize,
154    /// Cycles or self-loops in the re-export edge subgraph (barrel files
155    /// re-exporting from each other in a loop).
156    #[serde(default)]
157    pub re_export_cycles: usize,
158    /// Imports that cross architecture boundary rules.
159    pub boundary_violations: usize,
160    /// Files that match no architecture boundary zone.
161    #[serde(default)]
162    pub boundary_coverage_violations: usize,
163    /// Calls from zoned files to callees forbidden for that zone.
164    #[serde(default)]
165    pub boundary_call_violations: usize,
166    /// Banned calls, imports, and catalogue-derived effects matched by
167    /// declarative rule packs.
168    #[serde(default)]
169    pub policy_violations: usize,
170    /// Suppression comments that no longer match a finding.
171    pub stale_suppressions: usize,
172    /// Unused pnpm-workspace catalog entries.
173    pub unused_catalog_entries: usize,
174    /// Empty named catalog groups.
175    pub empty_catalog_groups: usize,
176    /// Workspace package.json catalog references the workspace catalogs
177    /// do not declare.
178    pub unresolved_catalog_references: usize,
179    /// Pnpm `overrides:` entries whose target package is not declared by any
180    /// workspace package and not present in the lockfile.
181    pub unused_dependency_overrides: usize,
182    /// Pnpm `overrides:` entries whose key or value cannot be parsed.
183    pub misconfigured_dependency_overrides: usize,
184    /// `"use client"` files that export a Next.js server-only / route-config name.
185    #[serde(default)]
186    pub invalid_client_exports: usize,
187    /// Barrel files that re-export both a `"use client"` origin and a
188    /// server-only origin.
189    #[serde(default)]
190    pub mixed_client_server_barrels: usize,
191    /// Misplaced `"use client"` / `"use server"` directives written as
192    /// expression statements after a non-directive statement.
193    #[serde(default)]
194    pub misplaced_directives: usize,
195    /// Next.js App Router route files that resolve to the same URL within one
196    /// app-root.
197    #[serde(default)]
198    pub route_collisions: usize,
199    /// Sibling Next.js dynamic route segments at one position using different
200    /// param spellings.
201    #[serde(default)]
202    pub dynamic_segment_name_conflicts: usize,
203}
204
205/// Per-category delta comparison against a saved baseline. Only present in
206/// `CheckOutput` when `--baseline` is used.
207#[derive(Debug, Clone, Default, Serialize)]
208#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
209pub struct BaselineDeltas {
210    /// Net change in total issues vs baseline (positive = more issues).
211    pub total_delta: i64,
212    /// Per-category breakdown of current, baseline, and delta counts.
213    pub per_category: BTreeMap<String, BaselineCategoryDelta>,
214}
215
216/// Single-category baseline delta entry inside [`BaselineDeltas::per_category`].
217#[derive(Debug, Clone, Copy, Default, Serialize)]
218#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
219pub struct BaselineCategoryDelta {
220    /// Current issue count for this category.
221    pub current: usize,
222    /// Baseline issue count for this category.
223    pub baseline: usize,
224    /// Change from baseline (current - baseline).
225    pub delta: i64,
226}
227
228/// Baseline match statistics. Shows how many baseline entries existed and how
229/// many matched current issues. Useful for detecting stale baselines
230/// programmatically. Only present in `CheckOutput` when `--baseline` is used.
231#[derive(Debug, Clone, Copy, Default, Serialize)]
232#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
233pub struct BaselineMatch {
234    /// Total number of entries in the loaded baseline file.
235    pub entries: usize,
236    /// Number of baseline entries that matched current issues and were
237    /// filtered.
238    pub matched: usize,
239}
240
241/// Result of regression detection (`--fail-on-regression`). Compares current
242/// issue counts against a baseline from config or an explicit file.
243#[derive(Debug, Clone, Serialize)]
244#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
245pub struct RegressionResult {
246    /// Outcome of the regression check.
247    pub status: RegressionStatus,
248    /// Baseline total before the change. Absent when status is `skipped`.
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub baseline_total: Option<i64>,
251    /// Current total after the change. Absent when status is `skipped`.
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub current_total: Option<i64>,
254    /// Difference current - baseline. Absent when status is `skipped`.
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub delta: Option<i64>,
257    /// Configured tolerance, interpreted per [`RegressionToleranceKind`].
258    /// Absent when status is `skipped`.
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub tolerance: Option<f64>,
261    /// Interpretation of the tolerance value.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub tolerance_kind: Option<RegressionToleranceKind>,
264    /// Whether the regression exceeded the tolerance.
265    pub exceeded: bool,
266    /// Only present when status is `skipped`.
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub reason: Option<String>,
269}
270
271/// Status of a regression-check pass.
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
274#[serde(rename_all = "lowercase")]
275pub enum RegressionStatus {
276    /// Issue count within tolerance.
277    Pass,
278    /// Issue count exceeded tolerance.
279    Exceeded,
280    /// Regression check did not run (missing baseline, etc.).
281    Skipped,
282}
283
284/// Interpretation of [`RegressionResult::tolerance`].
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
287#[serde(rename_all = "lowercase")]
288pub enum RegressionToleranceKind {
289    /// Tolerance is interpreted as an absolute issue-count delta.
290    Absolute,
291    /// Tolerance is interpreted as a percentage of the baseline total.
292    Percentage,
293}
294
295/// Metric and rule definitions emitted under `_meta` when `--explain` is
296/// passed (always present in MCP responses). Helps AI agents and CI systems
297/// interpret metric values without re-reading the docs site.
298#[derive(Debug, Clone, Default, Serialize)]
299#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
300pub struct Meta {
301    /// URL to the documentation page for this command.
302    #[serde(default, skip_serializing_if = "Option::is_none")]
303    pub docs: Option<String>,
304    /// Local telemetry correlation metadata for agent follow-up runs.
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub telemetry: Option<TelemetryMeta>,
307    /// Per-field definitions for envelope fields and action payload fields.
308    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
309    pub field_definitions: BTreeMap<String, String>,
310    /// Per-metric definitions: name, description, range, interpretation.
311    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
312    pub metrics: BTreeMap<String, MetaMetric>,
313    /// Per-rule definitions for check command output.
314    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
315    pub rules: BTreeMap<String, MetaRule>,
316}
317
318/// Privacy-safe local run metadata emitted for JSON consumers.
319#[derive(Debug, Clone, Default, Serialize)]
320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
321pub struct TelemetryMeta {
322    /// Ephemeral local token that may be passed to the hidden `--parent-run`
323    /// flag on a later command. It is not derived from repository, path, user,
324    /// machine, project, or cloud data.
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub analysis_run_id: Option<String>,
327}
328
329/// Single-metric definition inside [`Meta::metrics`].
330#[derive(Debug, Clone, Default, Serialize)]
331#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
332pub struct MetaMetric {
333    /// Human-readable metric name.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub name: Option<String>,
336    /// What this metric measures and how it is computed.
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub description: Option<String>,
339    /// Valid value range (e.g., `"[0, 100]"`).
340    #[serde(default, skip_serializing_if = "Option::is_none")]
341    pub range: Option<String>,
342    /// How to read the value (e.g., `"lower is better"`).
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    pub interpretation: Option<String>,
345}
346
347/// Single-rule definition inside [`Meta::rules`].
348#[derive(Debug, Clone, Default, Serialize)]
349#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
350pub struct MetaRule {
351    /// Human-readable rule name.
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub name: Option<String>,
354    /// What this rule detects.
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub description: Option<String>,
357    /// URL to the rule documentation.
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub docs: Option<String>,
360}