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