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::{Deserialize, Serialize};
17
18use crate::semantic::{
19    ApiSurfaceResult, SemanticAnalysisIdentity, SemanticCandidateDecision, SemanticGapReason,
20    SemanticQuerySummary, SemanticSymbolImpact, SemanticSymbolTrace, TypeCouplingReport,
21};
22
23/// Schema version for this output format (independent of tool version). Bump
24/// policy: ADDITIVE changes (new optional top-level fields, new optional struct
25/// fields, new array entries, new MCP tools, new CLI flags that map to new
26/// optional fields) do NOT bump the version; consumers receive new fields
27/// without breaking. BREAKING changes (renamed fields, removed fields, type
28/// changes, enum-variant removals, semantic changes to existing fields) DO
29/// bump. Additions to existing enum-valued required fields bump the affected
30/// envelope version so strict JSON Schema consumers can migrate.
31/// `ComplexityContributionKind` is non-exhaustive so Rust consumers retain a
32/// wildcard. To
33/// detect newly-added fields without a bump, check field presence via
34/// JSON-key existence rather than gating on the version. v4 was introduced
35/// alongside fallow-cov-protocol 0.2 (per-finding verdict, stable IDs, evidence
36/// block, renamed summary fields); v5 introduced health_score formula_version 2
37/// with scale-invariant scoring semantics; v6 widened `AddToConfigAction.value`
38/// from a scalar string to `oneOf: [string, array]` so the new `ignoreExports`
39/// action can carry a paste-ready array of `{ file, exports }` rule objects
40/// (the legacy `ignoreDependencies` etc. variants still emit strings, so
41/// consumers that switch on `config_key` keep working unchanged). v8 added the
42/// required duplication `spread` field and changed `duplicated_tokens` to count
43/// redundant copies, excluding the retained copy in each group. Envelopes
44/// embedding health use their own version marker, so health-only contract
45/// changes do not advance dead-code or unrelated sibling envelopes. The
46/// runtime-coverage block is extended additively as the protocol evolves
47/// (currently 0.3, which adds an optional capture_quality summary field). Other
48/// additive examples: dupes --group-by adds optional grouped_by, total_issues,
49/// groups fields without bumping.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[serde(transparent)]
53pub struct SchemaVersion(pub u32);
54
55/// Fallow CLI version that produced this envelope. Renders to the JSON wire as
56/// a bare string (e.g. `"2.74.0"`).
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
59#[serde(transparent)]
60pub struct ToolVersion(pub String);
61
62/// Analysis duration in milliseconds. Renders to the JSON wire as a bare
63/// integer.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66#[serde(transparent)]
67pub struct ElapsedMs(pub u64);
68
69/// Audit-mode marker emitted on each finding when `fallow audit --format json`
70/// runs with a base ref. `true` means the finding's structural key was not
71/// present at the base ref (introduced by the current changeset); `false`
72/// means it was inherited. Duplication findings carry one carve-out: a clone
73/// group whose structural key is new but whose instances contain no added line
74/// from the diff (a group re-shaped by removing duplication elsewhere) is
75/// demoted to inherited and serializes `false` (issue #2164).
76///
77/// Outside of audit sub-results the field is omitted, so call sites typically
78/// hold `Option<AuditIntroduced>`. Renders to the JSON wire as a bare boolean.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81#[serde(transparent)]
82pub struct AuditIntroduced(pub bool);
83
84/// Entry-point detection summary embedded in `CheckOutput` and the combined
85/// envelope.
86#[derive(Debug, Clone, Default, Serialize)]
87#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88pub struct EntryPoints {
89    /// Total number of detected entry points.
90    pub total: usize,
91    /// Breakdown of entry points by detection source (e.g., `"package.json"`,
92    /// `"next.js"`, `"config entry"`). Underscored keys so dashboards can
93    /// drill into individual sources.
94    pub sources: BTreeMap<String, usize>,
95}
96
97/// Per-category issue counts for dead-code analysis. Always present in
98/// `CheckOutput`; when `--summary` is used the individual issue arrays are
99/// omitted but this object stays populated.
100#[derive(Debug, Clone, Default, Serialize)]
101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
102pub struct CheckSummary {
103    /// Total number of issues across all categories.
104    pub total_issues: usize,
105    /// Unused source files.
106    pub unused_files: usize,
107    /// Unused value exports.
108    pub unused_exports: usize,
109    /// Unused type exports.
110    pub unused_types: usize,
111    /// Public exports whose signature references same-file private types.
112    pub private_type_leaks: usize,
113    /// Combined count of unused entries across `dependencies`,
114    /// `devDependencies`, and `optionalDependencies`. The per-section
115    /// breakdown lives in the individual issue arrays on `CheckOutput`.
116    pub unused_dependencies: usize,
117    /// Unused enum members.
118    pub unused_enum_members: usize,
119    /// Unused class members.
120    pub unused_class_members: usize,
121    /// Unused store members.
122    #[serde(default)]
123    pub unused_store_members: usize,
124    /// Vue/Svelte injects whose key is provided nowhere in the project.
125    #[serde(default)]
126    pub unprovided_injects: usize,
127    /// Vue/Svelte components reachable but rendered nowhere in the project.
128    #[serde(default)]
129    pub unrendered_components: usize,
130    /// Vue, Svelte, or React props referenced nowhere inside their own component.
131    #[serde(default)]
132    pub unused_component_props: usize,
133    /// Vue `<script setup>` emits emitted nowhere inside their own SFC.
134    #[serde(default)]
135    pub unused_component_emits: usize,
136    /// Angular `@Input()` bindings referenced nowhere inside their own component.
137    #[serde(default)]
138    pub unused_component_inputs: usize,
139    /// Angular `@Output()` bindings emitted nowhere inside their own component.
140    #[serde(default)]
141    pub unused_component_outputs: usize,
142    /// Svelte components dispatching a custom event via `createEventDispatcher`
143    /// whose name is listened to nowhere in the project.
144    #[serde(default)]
145    pub unused_svelte_events: usize,
146    /// Next.js Server Actions (exports of `"use server"` files) referenced by no
147    /// code in the project.
148    #[serde(default)]
149    pub unused_server_actions: usize,
150    /// SvelteKit `load()` return-object keys read by no consumer.
151    #[serde(default)]
152    pub unused_load_data_keys: usize,
153    /// Imports that could not be resolved against the project's module graph.
154    pub unresolved_imports: usize,
155    /// Dependencies imported but absent from `package.json`.
156    pub unlisted_dependencies: usize,
157    /// Same-named exports declared in more than one module.
158    pub duplicate_exports: usize,
159    /// Production dependencies only used via type-only imports (could be
160    /// devDependencies). Only populated in production mode.
161    pub type_only_dependencies: usize,
162    /// Production dependencies only imported by test files (could be
163    /// devDependencies).
164    pub test_only_dependencies: usize,
165    /// devDependencies imported by production source code with a runtime/value
166    /// import (should be promoted to dependencies).
167    pub dev_dependencies_in_production: usize,
168    /// Cycles detected in the import graph.
169    pub circular_dependencies: usize,
170    /// Cycles or self-loops in the re-export edge subgraph (barrel files
171    /// re-exporting from each other in a loop).
172    #[serde(default)]
173    pub re_export_cycles: usize,
174    /// Imports that cross architecture boundary rules.
175    pub boundary_violations: usize,
176    /// Files that match no architecture boundary zone.
177    #[serde(default)]
178    pub boundary_coverage_violations: usize,
179    /// Calls from zoned files to callees forbidden for that zone.
180    #[serde(default)]
181    pub boundary_call_violations: usize,
182    /// Banned calls, imports, and catalogue-derived effects matched by
183    /// declarative rule packs.
184    #[serde(default)]
185    pub policy_violations: usize,
186    /// Suppression comments that no longer match a finding.
187    pub stale_suppressions: usize,
188    /// Unused pnpm-workspace catalog entries.
189    pub unused_catalog_entries: usize,
190    /// Empty named catalog groups.
191    pub empty_catalog_groups: usize,
192    /// Workspace package.json catalog references the workspace catalogs
193    /// do not declare.
194    pub unresolved_catalog_references: usize,
195    /// Pnpm `overrides:` entries whose target package is not declared by any
196    /// workspace package and not present in the lockfile.
197    pub unused_dependency_overrides: usize,
198    /// Pnpm `overrides:` entries whose key or value cannot be parsed.
199    pub misconfigured_dependency_overrides: usize,
200    /// `"use client"` files that export a Next.js server-only / route-config name.
201    #[serde(default)]
202    pub invalid_client_exports: usize,
203    /// Barrel files that re-export both a `"use client"` origin and a
204    /// server-only origin.
205    #[serde(default)]
206    pub mixed_client_server_barrels: usize,
207    /// Misplaced `"use client"` / `"use server"` directives written as
208    /// expression statements after a non-directive statement.
209    #[serde(default)]
210    pub misplaced_directives: usize,
211    /// Next.js App Router route files that resolve to the same URL within one
212    /// app-root.
213    #[serde(default)]
214    pub route_collisions: usize,
215    /// Sibling Next.js dynamic route segments at one position using different
216    /// param spellings.
217    #[serde(default)]
218    pub dynamic_segment_name_conflicts: usize,
219}
220
221/// Per-category delta comparison against a saved baseline. Only present in
222/// `CheckOutput` when `--baseline` is used.
223#[derive(Debug, Clone, Default, Serialize)]
224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
225pub struct BaselineDeltas {
226    /// Net change in total issues vs baseline (positive = more issues).
227    pub total_delta: i64,
228    /// Per-category breakdown of current, baseline, and delta counts.
229    pub per_category: BTreeMap<String, BaselineCategoryDelta>,
230}
231
232/// Single-category baseline delta entry inside [`BaselineDeltas::per_category`].
233#[derive(Debug, Clone, Copy, Default, Serialize)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235pub struct BaselineCategoryDelta {
236    /// Current issue count for this category.
237    pub current: usize,
238    /// Baseline issue count for this category.
239    pub baseline: usize,
240    /// Change from baseline (current - baseline).
241    pub delta: i64,
242}
243
244/// Baseline match statistics. Shows how many baseline entries existed and how
245/// many matched current issues. Useful for detecting stale baselines
246/// programmatically. Only present in `CheckOutput` when `--baseline` is used.
247#[derive(Debug, Clone, Copy, Default, Serialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249pub struct BaselineMatch {
250    /// Total number of entries in the loaded baseline file.
251    pub entries: usize,
252    /// Number of baseline entries that matched current issues and were
253    /// filtered.
254    pub matched: usize,
255}
256
257/// Result of regression detection (`--fail-on-regression`). Compares current
258/// issue counts against a baseline from config or an explicit file.
259#[derive(Debug, Clone, Serialize)]
260#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
261pub struct RegressionResult {
262    /// Outcome of the regression check.
263    pub status: RegressionStatus,
264    /// Baseline total before the change. Absent when status is `skipped`.
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub baseline_total: Option<i64>,
267    /// Current total after the change. Absent when status is `skipped`.
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub current_total: Option<i64>,
270    /// Difference current - baseline. Absent when status is `skipped`.
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub delta: Option<i64>,
273    /// Configured tolerance, interpreted per [`RegressionToleranceKind`].
274    /// Absent when status is `skipped`.
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub tolerance: Option<f64>,
277    /// Interpretation of the tolerance value.
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub tolerance_kind: Option<RegressionToleranceKind>,
280    /// Whether the regression exceeded the tolerance.
281    pub exceeded: bool,
282    /// Only present when status is `skipped`.
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub reason: Option<String>,
285}
286
287/// Status of a regression-check pass.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
290#[serde(rename_all = "lowercase")]
291pub enum RegressionStatus {
292    /// Issue count within tolerance.
293    Pass,
294    /// Issue count exceeded tolerance.
295    Exceeded,
296    /// Regression check did not run (missing baseline, etc.).
297    Skipped,
298}
299
300/// Interpretation of [`RegressionResult::tolerance`].
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[serde(rename_all = "lowercase")]
304pub enum RegressionToleranceKind {
305    /// Tolerance is interpreted as an absolute issue-count delta.
306    Absolute,
307    /// Tolerance is interpreted as a percentage of the baseline total.
308    Percentage,
309}
310
311/// Metric and rule definitions emitted under `_meta` when `--explain` is
312/// passed (always present in MCP responses). Helps AI agents and CI systems
313/// interpret metric values without re-reading the docs site.
314#[derive(Debug, Clone, Default, Serialize)]
315#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
316pub struct Meta {
317    /// URL to the documentation page for this command.
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub docs: Option<String>,
320    /// Local telemetry correlation metadata for agent follow-up runs.
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub telemetry: Option<TelemetryMeta>,
323    /// Provenance for the opt-in TypeScript semantic analysis pass.
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub type_aware: Option<TypeAwareMeta>,
326    /// Per-field definitions for envelope fields and action payload fields.
327    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
328    pub field_definitions: BTreeMap<String, String>,
329    /// Per-metric definitions: name, description, range, interpretation.
330    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
331    pub metrics: BTreeMap<String, MetaMetric>,
332    /// Per-rule definitions for check command output.
333    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
334    pub rules: BTreeMap<String, MetaRule>,
335}
336
337/// Bounded provenance emitted when the opt-in type-aware pass runs.
338#[derive(Debug, Clone, Default, Serialize)]
339#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
340pub struct TypeAwareMeta {
341    /// Compatibility identity used by audit, baselines, snapshots, and stores.
342    #[serde(default, skip_serializing_if = "Option::is_none")]
343    pub identity: Option<SemanticAnalysisIdentity>,
344    /// Effective CLI or repository policy for incomplete semantic evidence.
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub required_completeness: Option<crate::semantic::SemanticCompletenessRequirement>,
347    /// Compact status for every requested semantic query.
348    #[serde(default, skip_serializing_if = "Vec::is_empty")]
349    pub queries: Vec<SemanticQuerySummary>,
350    /// Bounded decision and evidence for each semantic dead-code candidate.
351    #[serde(default, skip_serializing_if = "Vec::is_empty")]
352    pub candidate_decisions: Vec<SemanticCandidateDecision>,
353    /// Checker-backed trace evidence requested by focused symbol queries.
354    #[serde(default, skip_serializing_if = "Vec::is_empty")]
355    pub symbol_traces: Vec<SemanticSymbolTrace>,
356    /// Package-public surface and confirmed private type leaks.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub api_surface: Option<ApiSurfaceResult>,
359    /// Exact-symbol blast radius and targeted-test recommendations.
360    #[serde(default, skip_serializing_if = "Vec::is_empty")]
361    pub symbol_impacts: Vec<SemanticSymbolImpact>,
362    /// Advisory project-local public-signature coupling.
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub type_coupling: Option<TypeCouplingReport>,
365    /// Whether the semantic companion executed at least one query.
366    pub executed: bool,
367    /// Version of Fallow's backend-neutral sidecar protocol.
368    pub protocol_version: u32,
369    /// Version of the sidecar package that executed the query.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub sidecar_version: Option<String>,
372    /// Semantic backend capability identifier.
373    pub backend: String,
374    /// Backend compiler or engine version that executed the query.
375    #[serde(default, skip_serializing_if = "Option::is_none")]
376    pub backend_version: Option<String>,
377    /// TypeScript project configs selected for candidate files.
378    pub selected_tsconfigs: Vec<String>,
379    /// Number of candidate findings sent to the sidecar.
380    pub candidate_count: usize,
381    /// Number of candidates confirmed as used and removed.
382    pub confirmed_used_count: usize,
383    /// Number of candidates preserved because they implement or override a contract.
384    pub contract_preserved_count: usize,
385    /// Number of candidates with complete, closed-world no-static-reference evidence.
386    pub no_static_references_count: usize,
387    /// Number of retained class members eligible for a guarded type-aware fix.
388    pub fix_eligible_count: usize,
389    /// Number of candidates retained because semantic use was unresolved.
390    pub unresolved_count: usize,
391    /// Number of candidates retained because semantic analysis abstained.
392    pub abstained_count: usize,
393    /// Stable abstention reason counts for automation and diagnostics.
394    pub abstention_reasons: TypeAwareAbstentionCounts,
395    /// Per-project semantic refinement status and evidence.
396    pub projects: Vec<TypeAwareProjectMeta>,
397    /// Number of bounded warnings returned by the sidecar.
398    pub warning_count: usize,
399    /// Bounded semantic warnings. Findings mentioned here were retained.
400    pub warnings: Vec<String>,
401    /// Semantic pass duration as reported by the sidecar.
402    pub elapsed_ms: u64,
403    /// Bounded semantic phase timings reported by the sidecar.
404    pub phase_timings_ms: TypeAwarePhaseTimings,
405}
406
407/// Closed set of reasons for retaining a candidate without semantic scanning.
408#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
409#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
410#[serde(rename_all = "kebab-case")]
411pub enum TypeAwareAbstentionReason {
412    /// No selected TypeScript project contains the candidate file.
413    #[default]
414    NoProject,
415    /// Multiple explicit TypeScript projects contain the candidate file.
416    AmbiguousProject,
417    /// Structural TypeScript diagnostics make exact matching unsafe.
418    BlockingDiagnostics,
419}
420
421/// Closed abstention reason counts for stable machine consumption.
422#[derive(Debug, Clone, Default, Serialize)]
423#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
424pub struct TypeAwareAbstentionCounts {
425    /// Candidates not contained by a selected TypeScript project.
426    pub no_project: usize,
427    /// Candidates contained by more than one explicit TypeScript project.
428    pub ambiguous_project: usize,
429    /// Candidates retained because structural diagnostics block scanning.
430    pub blocking_diagnostics: usize,
431    /// Candidates whose exact declaration identity could not be resolved.
432    pub unknown_symbol: usize,
433    /// Candidates using declaration syntax unsupported by the semantic backend.
434    pub unsupported_syntax: usize,
435    /// Candidates retained because the bounded semantic request reached capacity.
436    pub capacity: usize,
437}
438
439/// How a TypeScript project was selected for semantic refinement.
440#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
441#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
442#[serde(rename_all = "kebab-case")]
443pub enum TypeAwareProjectSource {
444    /// Fallow selected the nearest discovered project automatically.
445    #[default]
446    Auto,
447    /// The project was supplied with `--type-aware-project`.
448    Explicit,
449}
450
451/// Outcome of semantic refinement for one TypeScript project.
452#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
453#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
454#[serde(rename_all = "kebab-case")]
455pub enum TypeAwareProjectStatus {
456    /// The project was structurally safe and its candidates were scanned.
457    #[default]
458    Refined,
459    /// Structural diagnostics prevented candidate scanning.
460    Abstained,
461    /// All semantic queries assigned to this Program completed.
462    Complete,
463    /// The Program could not answer its assigned semantic queries safely.
464    Unavailable,
465}
466
467/// How a persistent semantic snapshot was refreshed.
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
469#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
470#[serde(rename_all = "kebab-case")]
471pub enum TypeAwareInvalidationKind {
472    /// The backend rebuilt project state from a clean snapshot.
473    Full,
474    /// The backend applied an explicit source-file change set.
475    Incremental,
476    /// No filesystem change was reported between compatible requests.
477    None,
478}
479
480/// Semantic sidecar timings, separated from Fallow's syntactic pipeline.
481#[derive(Debug, Clone, Default, Serialize)]
482#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
483pub struct TypeAwarePhaseTimings {
484    /// TypeScript API construction and project snapshot selection.
485    pub project_setup: u64,
486    /// TypeScript diagnostics collected before any candidate refinement.
487    pub diagnostics: u64,
488    /// Batched symbol lookup and exact declaration matching.
489    pub symbol_scan: u64,
490}
491
492/// Bounded provenance for one TypeScript project handled by the sidecar.
493#[derive(Debug, Clone, Default, Serialize)]
494#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
495pub struct TypeAwareProjectMeta {
496    /// Project config relative to the analysis root, or `<inferred>`.
497    pub config: String,
498    /// How the project was selected: `auto` or `explicit`.
499    pub source: TypeAwareProjectSource,
500    /// Project result: `refined`, `abstained`, `complete`, or `unavailable`.
501    pub status: TypeAwareProjectStatus,
502    /// Candidates assigned to this project.
503    pub candidate_count: usize,
504    /// Candidates confirmed as used and removed.
505    pub confirmed_used_count: usize,
506    /// Candidates retained because they implement or override a contract.
507    pub contract_preserved_count: usize,
508    /// Candidates with complete no-static-reference evidence.
509    pub no_static_references_count: usize,
510    /// Candidates eligible for a guarded class-member fix.
511    pub fix_eligible_count: usize,
512    /// Candidates whose exact semantic outcome remained unresolved.
513    pub unresolved_count: usize,
514    /// Candidates retained without scanning because the project was unsafe.
515    pub abstained_count: usize,
516    /// Config, program, syntactic, and bind diagnostics that block scanning.
517    pub blocking_diagnostic_count: usize,
518    /// Source files loaded into this TypeScript program.
519    pub source_file_count: usize,
520    /// Whether this Program served more than one semantic query in the batch.
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub program_reused: Option<bool>,
523    /// Whether this Program served more than one query in the current batch.
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub program_shared_across_queries: Option<bool>,
526    /// Whether the root-bound semantic session reused the prior snapshot.
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub program_reused_from_previous_snapshot: Option<bool>,
529    /// Monotonic revision within the root-bound semantic session.
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub snapshot_revision: Option<u64>,
532    /// Full, incremental, or no invalidation before this query.
533    #[serde(default, skip_serializing_if = "Option::is_none")]
534    pub invalidation_kind: Option<TypeAwareInvalidationKind>,
535    /// Stable project-level gap reason.
536    #[serde(default, skip_serializing_if = "Option::is_none")]
537    pub reason_code: Option<SemanticGapReason>,
538    /// Stable reason code when `status` is `abstained`.
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    #[cfg_attr(feature = "schema", schemars(with = "TypeAwareAbstentionReason"))]
541    pub abstain_reason: Option<TypeAwareAbstentionReason>,
542}
543
544/// Privacy-safe local run metadata emitted for JSON consumers.
545#[derive(Debug, Clone, Default, Serialize)]
546#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
547pub struct TelemetryMeta {
548    /// Ephemeral local token that may be passed to the hidden `--parent-run`
549    /// flag on a later command. It is not derived from repository, path, user,
550    /// machine, project, or cloud data.
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub analysis_run_id: Option<String>,
553}
554
555/// Single-metric definition inside [`Meta::metrics`].
556#[derive(Debug, Clone, Default, Serialize)]
557#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
558pub struct MetaMetric {
559    /// Human-readable metric name.
560    #[serde(default, skip_serializing_if = "Option::is_none")]
561    pub name: Option<String>,
562    /// What this metric measures and how it is computed.
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    pub description: Option<String>,
565    /// Valid value range (e.g., `"[0, 100]"`).
566    #[serde(default, skip_serializing_if = "Option::is_none")]
567    pub range: Option<String>,
568    /// How to read the value (e.g., `"lower is better"`).
569    #[serde(default, skip_serializing_if = "Option::is_none")]
570    pub interpretation: Option<String>,
571}
572
573/// Single-rule definition inside [`Meta::rules`].
574#[derive(Debug, Clone, Default, Serialize)]
575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
576pub struct MetaRule {
577    /// Human-readable rule name.
578    #[serde(default, skip_serializing_if = "Option::is_none")]
579    pub name: Option<String>,
580    /// What this rule detects.
581    #[serde(default, skip_serializing_if = "Option::is_none")]
582    pub description: Option<String>,
583    /// URL to the rule documentation.
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub docs: Option<String>,
586}