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