fallow_output/health_runtime_coverage.rs
1use std::fmt;
2use std::path::PathBuf;
3
4use fallow_types::serde_path;
5
6/// Runtime coverage JSON contract version. This is scoped to the
7/// `runtime_coverage` block and is independent of the top-level fallow
8/// JSON `schema_version`.
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11pub enum RuntimeCoverageSchemaVersion {
12 /// First release of the runtime coverage block contract.
13 #[default]
14 #[serde(rename = "1")]
15 V1,
16}
17
18/// Top-level verdict for the whole runtime-coverage report. Mirrors
19/// `fallow_cov_protocol::ReportVerdict`. The verdict is the SINGLE most
20/// actionable finding; for the full set of findings see
21/// [`RuntimeCoverageReport::signals`]. The verdict promotes `hot-path-touched`
22/// above `cold-code-detected` in PR-review context (when the CLI was
23/// given a change-scope: `--diff-file` or `--changed-since`) because the
24/// touched-hot-path is event-tied to the current diff and reviewers need
25/// it to be the top-line signal. In standalone analysis (no change
26/// scope), `cold-code-detected` remains primary.
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[serde(rename_all = "kebab-case")]
30pub enum RuntimeCoverageReportVerdict {
31 /// No actionable runtime-coverage signal.
32 Clean,
33 /// The current change touches a runtime hot path.
34 HotPathTouched,
35 /// Runtime-cold code was detected.
36 ColdCodeDetected,
37 /// The license expired; output is in grace mode.
38 LicenseExpiredGrace,
39 /// No verdict could be derived.
40 #[default]
41 Unknown,
42}
43
44/// Discrete signal captured during runtime-coverage post-processing.
45/// `verdict` collapses to one summary value; `signals` enumerates ALL
46/// findings the report carries so JSON consumers, CI dashboards, and
47/// agents can reason about them independently of the headline. Order is
48/// stable: severity-descending so the first entry mirrors a sensible
49/// non-PR-context verdict.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[serde(rename_all = "kebab-case")]
53pub enum RuntimeCoverageSignal {
54 /// The license expired; output is in grace mode.
55 LicenseExpiredGrace,
56 /// Runtime-cold code was detected.
57 ColdCodeDetected,
58 /// The current change touches a runtime hot path.
59 HotPathTouched,
60}
61
62impl RuntimeCoverageSignal {
63 /// Kebab-case wire value of the signal.
64 #[must_use]
65 pub const fn as_str(self) -> &'static str {
66 match self {
67 Self::LicenseExpiredGrace => "license-expired-grace",
68 Self::ColdCodeDetected => "cold-code-detected",
69 Self::HotPathTouched => "hot-path-touched",
70 }
71 }
72}
73
74impl fmt::Display for RuntimeCoverageSignal {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80impl RuntimeCoverageReportVerdict {
81 /// Kebab-case wire value of the verdict.
82 #[must_use]
83 pub const fn as_str(self) -> &'static str {
84 match self {
85 Self::Clean => "clean",
86 Self::HotPathTouched => "hot-path-touched",
87 Self::ColdCodeDetected => "cold-code-detected",
88 Self::LicenseExpiredGrace => "license-expired-grace",
89 Self::Unknown => "unknown",
90 }
91 }
92}
93
94impl fmt::Display for RuntimeCoverageReportVerdict {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100/// Protocol-level per-function runtime coverage verdict derived from the
101/// decision table in fallow-cov-protocol. The CLI's `runtime_coverage.findings`
102/// array omits `active` entries even though the underlying enum still includes
103/// it.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[serde(rename_all = "snake_case")]
107pub enum RuntimeCoverageVerdict {
108 /// Statically unused and never invoked; delete candidate.
109 SafeToDelete,
110 /// Signals conflict; a human should review before acting.
111 ReviewRequired,
112 /// The function could not be tracked, so no runtime verdict exists.
113 CoverageUnavailable,
114 /// Tracked and invoked, but below the low-traffic threshold.
115 LowTraffic,
116 /// Tracked and actively invoked; omitted from `findings`.
117 Active,
118 /// No verdict could be derived.
119 Unknown,
120}
121
122impl RuntimeCoverageVerdict {
123 /// Snake-case wire value of the verdict.
124 #[must_use]
125 pub const fn as_str(self) -> &'static str {
126 match self {
127 Self::SafeToDelete => "safe_to_delete",
128 Self::ReviewRequired => "review_required",
129 Self::CoverageUnavailable => "coverage_unavailable",
130 Self::LowTraffic => "low_traffic",
131 Self::Active => "active",
132 Self::Unknown => "unknown",
133 }
134 }
135
136 /// Space-separated label for human-readable output.
137 #[must_use]
138 pub const fn human_label(self) -> &'static str {
139 match self {
140 Self::SafeToDelete => "safe to delete",
141 Self::ReviewRequired => "review required",
142 Self::CoverageUnavailable => "coverage unavailable",
143 Self::LowTraffic => "low traffic",
144 Self::Active => "active",
145 Self::Unknown => "unknown",
146 }
147 }
148}
149
150impl fmt::Display for RuntimeCoverageVerdict {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.write_str(self.as_str())
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158#[serde(rename_all = "snake_case")]
159/// Confidence level for a runtime coverage finding.
160pub enum RuntimeCoverageConfidence {
161 /// All evidence agrees and the observation volume is deep.
162 VeryHigh,
163 /// Strong evidence with adequate observation volume.
164 High,
165 /// Evidence is consistent but the observation volume is limited.
166 Medium,
167 /// Sparse or conflicting evidence.
168 Low,
169 /// No usable evidence for this finding.
170 None,
171 /// Confidence could not be derived.
172 Unknown,
173}
174
175impl RuntimeCoverageConfidence {
176 /// Snake-case wire value of the confidence level.
177 #[must_use]
178 pub const fn as_str(self) -> &'static str {
179 match self {
180 Self::VeryHigh => "very_high",
181 Self::High => "high",
182 Self::Medium => "medium",
183 Self::Low => "low",
184 Self::None => "none",
185 Self::Unknown => "unknown",
186 }
187 }
188}
189
190impl fmt::Display for RuntimeCoverageConfidence {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 f.write_str(self.as_str())
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
197#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
198#[serde(rename_all = "kebab-case")]
199/// License or trial watermark applied to runtime coverage output.
200pub enum RuntimeCoverageWatermark {
201 /// The trial period ended.
202 TrialExpired,
203 /// The license expired but grace-mode output continues.
204 LicenseExpiredGrace,
205 /// Watermark state could not be derived.
206 Unknown,
207}
208
209impl RuntimeCoverageWatermark {
210 /// Kebab-case wire value of the watermark.
211 #[must_use]
212 pub const fn as_str(self) -> &'static str {
213 match self {
214 Self::TrialExpired => "trial-expired",
215 Self::LicenseExpiredGrace => "license-expired-grace",
216 Self::Unknown => "unknown",
217 }
218 }
219}
220
221impl fmt::Display for RuntimeCoverageWatermark {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 f.write_str(self.as_str())
224 }
225}
226
227/// Runtime coverage source used to produce the summary.
228#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
229#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
230#[serde(rename_all = "snake_case")]
231pub enum RuntimeCoverageDataSource {
232 /// Locally supplied runtime coverage artifact.
233 #[default]
234 Local,
235 /// Runtime context pulled from fallow.cloud after explicit opt-in.
236 Cloud,
237}
238
239impl RuntimeCoverageDataSource {
240 /// Snake-case wire value of the data source.
241 #[must_use]
242 pub const fn as_str(self) -> &'static str {
243 match self {
244 Self::Local => "local",
245 Self::Cloud => "cloud",
246 }
247 }
248}
249
250impl fmt::Display for RuntimeCoverageDataSource {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.write_str(self.as_str())
253 }
254}
255
256/// Summary block mirroring `fallow_cov_protocol::Summary` (0.3 shape).
257#[derive(Debug, Clone, Default, serde::Serialize)]
258#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
259pub struct RuntimeCoverageSummary {
260 /// Runtime evidence source used for this report. Local mode reads a
261 /// supplied runtime coverage artifact; cloud mode pulls the latest
262 /// fallow.cloud runtime context after explicit opt-in.
263 pub data_source: RuntimeCoverageDataSource,
264 /// Timestamp of the newest runtime payload included in the report. Null for
265 /// local single-capture artifacts that do not carry cloud receipt metadata.
266 pub last_received_at: Option<String>,
267 /// Number of functions the sidecar could observe in the V8 or Istanbul
268 /// dump.
269 pub functions_tracked: usize,
270 /// Tracked functions that received at least one invocation.
271 pub functions_hit: usize,
272 /// Tracked functions that were never invoked.
273 pub functions_unhit: usize,
274 /// Functions the sidecar could not track (lazy-parsed, worker thread,
275 /// dynamic code, unresolved source map).
276 pub functions_untracked: usize,
277 /// Ratio of functions_hit / functions_tracked, expressed as a percent.
278 pub coverage_percent: f64,
279 /// Total number of observed invocations across all functions. Denominator
280 /// for low-traffic classification.
281 pub trace_count: u64,
282 /// Days of observation covered by the supplied dump (Phase 2 local analysis
283 /// emits 0, set by the beacon/cloud in Phase 3+).
284 pub period_days: u32,
285 /// Distinct deployments contributing to the supplied dump (Phase 2 local
286 /// analysis emits 0).
287 pub deployments_seen: u32,
288 /// Capture-quality telemetry. `None` for protocol-0.2 sidecars; protocol-0.3+
289 /// sidecars always populate it. Fuels the human-output short-window warning
290 /// and the quantified trial CTA, and is passed through to JSON consumers so
291 /// agent pipelines can surface the same signal.
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub capture_quality: Option<RuntimeCoverageCaptureQuality>,
294}
295
296/// Quality-of-capture signals emitted by the sidecar so the CLI can explain
297/// short-window captures honestly instead of letting users blame the tool.
298#[derive(Debug, Clone, PartialEq, serde::Serialize)]
299#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
300pub struct RuntimeCoverageCaptureQuality {
301 /// Total observation window in seconds. Finer-grained than period_days
302 /// (which rounds up to whole days).
303 pub window_seconds: u64,
304 /// Number of distinct production instances that contributed to the dump.
305 pub instances_observed: u32,
306 /// True when the untracked-function ratio exceeds the sidecar's lazy-parse
307 /// threshold (30%). Signals that many untracked functions likely reflect
308 /// lazy-parsed code rather than unreachable code.
309 pub lazy_parse_warning: bool,
310 /// functions_untracked / functions_tracked as a percentage, rounded to 2
311 /// decimal places.
312 pub untracked_ratio_percent: f64,
313}
314
315/// Supporting evidence for a finding (mirrors `fallow_cov_protocol::Evidence`).
316#[derive(Debug, Clone, serde::Serialize)]
317#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
318pub struct RuntimeCoverageEvidence {
319 /// `used` when the function is reachable in the module graph, `unused`
320 /// otherwise.
321 pub static_status: String,
322 /// `covered` when the project's test suite hits this function,
323 /// `not_covered` otherwise.
324 pub test_coverage: String,
325 /// `true` when the function is unreachable in the production module graph
326 /// but still referenced from a file that production mode excludes (test,
327 /// spec, story, fixture, or benchmark). Such a function is not dead code:
328 /// removing it breaks the referencing test. `false` when the production
329 /// graph was compared against the full tree and no such reference exists.
330 /// `null` when the report was produced without a production filter, or by
331 /// a surface that carries no second reachability answer.
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 #[cfg_attr(feature = "schema", schemars(default))]
334 pub test_only_reference: Option<bool>,
335 /// `tracked` when V8 observed the function, `untracked` otherwise.
336 pub v8_tracking: String,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 /// Reason the function is untracked. Populated only when v8_tracking is
339 /// `untracked`. Values: `lazy_parsed`, `worker_thread`, `dynamic_eval`,
340 /// `unknown`.
341 pub untracked_reason: Option<String>,
342 /// Days of observation backing this finding.
343 pub observation_days: u32,
344 /// Distinct deployments backing this finding.
345 pub deployments_observed: u32,
346}
347
348#[derive(Debug, Clone, serde::Serialize)]
349#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
350/// Suggested follow-up action for a runtime coverage finding.
351pub struct RuntimeCoverageAction {
352 /// Action identifier, normalized to `type` in JSON output. Known values
353 /// emitted by `fallow coverage analyze`: `delete-cold-code`
354 /// (verdict=safe_to_delete), `review-runtime` (verdict=review_required).
355 /// The sidecar may emit additional protocol-specific identifiers;
356 /// consumers should treat unknown values as forward-compat extensions.
357 #[serde(rename = "type")]
358 pub kind: String,
359 /// Human-readable action description.
360 pub description: String,
361 /// Whether fallow can apply this action automatically.
362 pub auto_fixable: bool,
363}
364
365/// Non-fatal diagnostic emitted while merging runtime coverage.
366#[derive(Debug, Clone, serde::Serialize)]
367#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
368pub struct RuntimeCoverageMessage {
369 /// Stable machine-readable warning code.
370 pub code: String,
371 /// Human-readable warning message.
372 pub message: String,
373}
374
375/// Discriminator inputs that PRODUCED a finding's verdict (fallow-rs/fallow-cloud#321),
376/// emitted alongside the verdict so an agent can reproduce it and see the
377/// minimum-observation confidence cap instead of re-deriving them from scratch.
378/// F4: these make the EXISTING Fallow-owned discriminators legible; they are not
379/// a new or external signal and gate nothing. Pairs with `evidence.static_status`
380/// (the static half of the discriminator set).
381#[derive(Debug, Clone, serde::Serialize)]
382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
383pub struct RuntimeCoverageDiscriminators {
384 /// Three-state runtime tracking: `called` (invocations > 0), `never_called`
385 /// (V8 tracked it, invocations == 0), or `untracked` (V8 never saw it). The
386 /// ONLY signal that can issue a deletion verdict.
387 pub tracking_state: String,
388 /// `invocations / trace_count` for this function; `null` when untracked (no
389 /// invocation count). The per-function value behind the low-traffic split.
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 #[cfg_attr(feature = "schema", schemars(default))]
392 pub invocation_ratio: Option<f64>,
393 /// Active/low_traffic split ratio in effect (CLI default 0.001). A tracked
394 /// function whose `invocation_ratio` is below this reads `low_traffic`, else
395 /// `active`.
396 pub low_traffic_threshold: f64,
397 /// Total observed invocations across all functions (the `invocation_ratio`
398 /// denominator), echoed per finding so the verdict is self-contained.
399 pub trace_count: u64,
400 /// High-confidence verdict floor (CLI default 5000). When `trace_count` is
401 /// below it, confidence is capped regardless of the per-function signal.
402 pub min_observation_volume: u32,
403 /// `trace_count >= min_observation_volume`: whether the dump cleared the
404 /// confidence floor. `false` means this verdict's confidence is capped.
405 pub meets_observation_volume: bool,
406}
407
408/// One per-function runtime-coverage finding in `runtime_coverage.findings`.
409#[derive(Debug, Clone, serde::Serialize)]
410#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
411pub struct RuntimeCoverageFinding {
412 /// Per-finding suppression key of the form `fallow:prod:<hash>` (first 8 hex
413 /// of SHA-256(file + function + line + 'prod')). Hashes the current line, so
414 /// it changes when the function moves. Use this to suppress one finding.
415 pub id: String,
416 /// Cross-surface join key of the form `fallow:fn:<hash>`
417 /// (`fallow_cov_protocol::function_identity_id`, hashes file + name +
418 /// start_line). The same function shares ONE value across findings, hot
419 /// paths, blast-radius, and importance entries (the per-finding `id` uses a
420 /// per-surface salt, so it differs by surface), and across V8, Istanbul,
421 /// and oxc producers (columns are excluded from the hash). Like `id`, it
422 /// changes when the function's file, name, or start line changes; it is a
423 /// cross-surface / cross-producer join key, not a line-move-immune one.
424 /// `null` when the producing surface (or an un-migrated cloud) supplied no
425 /// `FunctionIdentity`.
426 #[serde(default, skip_serializing_if = "Option::is_none")]
427 #[cfg_attr(feature = "schema", schemars(default))]
428 pub stable_id: Option<String>,
429 /// Content digest of the function's full-span source slice
430 /// (`fallow_cov_protocol::source_hash_for`: first 8 bytes of SHA-256 as 16
431 /// lowercase hex). Unlike `stable_id`, this is stable across line moves: a
432 /// moved-but-unedited function keeps the same value, so baselines can
433 /// suppress it after a pure line shift. `null` when the producing surface
434 /// supplied no `source_hash`.
435 #[serde(default, skip_serializing_if = "Option::is_none")]
436 #[cfg_attr(feature = "schema", schemars(default))]
437 pub source_hash: Option<String>,
438 /// File path relative to the project root.
439 #[serde(serialize_with = "serde_path::serialize")]
440 pub path: PathBuf,
441 /// Static function name as reported in the merged coverage result.
442 pub function: String,
443 /// 1-indexed line number the function starts on.
444 pub line: u32,
445 /// Per-function runtime verdict from the protocol decision table.
446 pub verdict: RuntimeCoverageVerdict,
447 /// Raw V8 invocation count. `None` when the function was untracked
448 /// (lazy-parsed, worker thread, or dynamic code).
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub invocations: Option<u64>,
451 /// Confidence in the verdict.
452 pub confidence: RuntimeCoverageConfidence,
453 /// Static, test, and tracking evidence behind the verdict.
454 pub evidence: RuntimeCoverageEvidence,
455 #[serde(default, skip_serializing_if = "Vec::is_empty")]
456 #[cfg_attr(feature = "schema", schemars(default))]
457 /// Suggested actions for this finding. Omitted when empty.
458 pub actions: Vec<RuntimeCoverageAction>,
459 /// The discriminator inputs that produced this verdict (#321), emitted so an
460 /// agent can reproduce it and see the confidence cap. `None` for findings
461 /// not built from the merge pipeline (e.g. baseline round-trips). Omitted
462 /// from JSON when absent.
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 #[cfg_attr(feature = "schema", schemars(default))]
465 pub discriminators: Option<RuntimeCoverageDiscriminators>,
466}
467
468/// One hot function in `runtime_coverage.hot_paths`, ranked by invocations.
469#[derive(Debug, Clone, serde::Serialize)]
470#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
471pub struct RuntimeCoverageHotPath {
472 /// Stable content-hash ID of the form `fallow:hot:<hash>`.
473 pub id: String,
474 /// Cross-surface join key (`fallow:fn:<hash>`) for the hot function. Stable
475 /// across line moves; shared with the same function's findings / blast /
476 /// importance entries. `null` when no `FunctionIdentity` was supplied.
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 #[cfg_attr(feature = "schema", schemars(default))]
479 pub stable_id: Option<String>,
480 /// File path relative to the project root.
481 #[serde(serialize_with = "serde_path::serialize")]
482 pub path: PathBuf,
483 /// Function name for the hot path.
484 pub function: String,
485 /// 1-indexed line number the function starts on.
486 pub line: u32,
487 /// 1-indexed line the function ends on (inclusive). Mirrors
488 /// `fallow_cov_protocol::HotPath::end_line` (added in protocol 0.5).
489 /// Older 0.4-shape sidecars omit the field on the wire; serde defaults
490 /// to `0`, which the line-overlap filter MUST treat as a single-line
491 /// range (`line..=line`) rather than a span.
492 pub end_line: u32,
493 /// Observed invocation count for the hot path.
494 pub invocations: u64,
495 /// Percentile rank over this response's hot-path distribution. `100`
496 /// means the busiest, `0` means the quietest function that qualified.
497 pub percentile: u8,
498 #[serde(default, skip_serializing_if = "Vec::is_empty")]
499 #[cfg_attr(feature = "schema", schemars(default))]
500 /// Suggested actions for this hot path (e.g., review-on-change). Omitted
501 /// when empty.
502 pub actions: Vec<RuntimeCoverageAction>,
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
506#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
507#[serde(rename_all = "snake_case")]
508/// Blast-radius risk band. The current thresholds are high at >=20 static
509/// callers or >=1,000,000 traffic-weighted caller reach; medium at >=5 callers
510/// or >=50,000 weighted reach; low otherwise.
511pub enum RuntimeCoverageRiskBand {
512 /// Small caller footprint.
513 Low,
514 /// Moderate caller footprint.
515 Medium,
516 /// Wide caller footprint; changes ripple far.
517 High,
518}
519
520impl RuntimeCoverageRiskBand {
521 /// Snake-case wire value of the risk band.
522 #[must_use]
523 pub const fn as_str(self) -> &'static str {
524 match self {
525 Self::Low => "low",
526 Self::Medium => "medium",
527 Self::High => "high",
528 }
529 }
530}
531
532impl fmt::Display for RuntimeCoverageRiskBand {
533 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534 f.write_str(self.as_str())
535 }
536}
537
538/// One blast-radius entry in `runtime_coverage.blast_radius`: how far a
539/// change to the function would ripple.
540#[derive(Debug, Clone, serde::Serialize)]
541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
542pub struct RuntimeCoverageBlastRadiusEntry {
543 /// Stable content-hash ID of the form `fallow:blast:<hash>`.
544 pub id: String,
545 /// Cross-surface join key (`fallow:fn:<hash>`) for the function. Stable
546 /// across line moves; shared with the same function's findings / hot-path /
547 /// importance entries. `null` when no `FunctionIdentity` was supplied.
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 #[cfg_attr(feature = "schema", schemars(default))]
550 pub stable_id: Option<String>,
551 /// File path relative to the project root.
552 #[serde(serialize_with = "serde_path::serialize")]
553 pub file: PathBuf,
554 /// Function name for the blast-radius entry.
555 pub function: String,
556 /// 1-indexed line number the function starts on.
557 pub line: u32,
558 /// Static caller count from the module graph.
559 pub caller_count: u32,
560 /// Caller reach weighted by observed runtime traffic.
561 pub caller_count_weighted_by_traffic: u64,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
563 /// Distinct deploy SHAs that touched the function in the observation
564 /// window. Cloud mode only; omitted in local mode.
565 pub deploys_touched: Option<u32>,
566 /// Risk band derived from caller counts and traffic-weighted reach.
567 pub risk_band: RuntimeCoverageRiskBand,
568}
569
570/// One production-importance entry in `runtime_coverage.importance`, scoring
571/// how much a function matters in production.
572#[derive(Debug, Clone, serde::Serialize)]
573#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
574pub struct RuntimeCoverageImportanceEntry {
575 /// Stable content-hash ID of the form `fallow:importance:<hash>`.
576 pub id: String,
577 /// Cross-surface join key (`fallow:fn:<hash>`) for the function. Stable
578 /// across line moves; shared with the same function's findings / hot-path /
579 /// blast-radius entries. `null` when no `FunctionIdentity` was supplied.
580 #[serde(default, skip_serializing_if = "Option::is_none")]
581 #[cfg_attr(feature = "schema", schemars(default))]
582 pub stable_id: Option<String>,
583 /// File path relative to the project root.
584 #[serde(serialize_with = "serde_path::serialize")]
585 pub file: PathBuf,
586 /// Function name for the importance entry.
587 pub function: String,
588 /// 1-indexed line number the function starts on.
589 pub line: u32,
590 /// Observed invocation count for this function.
591 pub invocations: u64,
592 /// Cyclomatic complexity from the static health pipeline.
593 pub cyclomatic: u32,
594 /// Number of CODEOWNERS owners matched for this file. Zero means no owner
595 /// was resolved.
596 pub owner_count: u32,
597 /// 0-100 explainable score from log-scaled traffic, capped complexity
598 /// weight, and ownership-risk weight.
599 pub importance_score: f64,
600 /// Templated one-sentence explanation for the score.
601 pub reason: String,
602}
603
604#[derive(Debug, Clone, Default, serde::Serialize)]
605#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
606/// Runtime coverage findings merged into the health report or emitted by
607/// `fallow coverage analyze`. Present in health output when --runtime-coverage
608/// is used. Shape mirrors the runtime coverage JSON contract; cloud mode
609/// fetches runtime facts explicitly and merges them locally with AST/static
610/// analysis.
611pub struct RuntimeCoverageReport {
612 /// Runtime coverage JSON contract version. This is scoped to the
613 /// `runtime_coverage` block and is independent of the top-level fallow
614 /// JSON `schema_version`.
615 pub schema_version: RuntimeCoverageSchemaVersion,
616 /// Single most actionable runtime-coverage signal under the current
617 /// context. In PR-review context (CLI saw `--diff-file` or
618 /// `--changed-since`) the verdict is `hot-path-touched` whenever a hot
619 /// function was touched, regardless of cold-code findings; in standalone
620 /// analysis `cold-code-detected` remains primary. For the full set of
621 /// findings the report carries, see `signals`.
622 pub verdict: RuntimeCoverageReportVerdict,
623 /// All signals captured by post-processing. Independent of `verdict`,
624 /// which is the single most actionable signal under the current
625 /// context. Empty when the report is `Clean` and not under license
626 /// grace. Order is stable severity-descending.
627 #[serde(default, skip_serializing_if = "Vec::is_empty")]
628 #[cfg_attr(feature = "schema", schemars(default))]
629 pub signals: Vec<RuntimeCoverageSignal>,
630 /// Aggregate tracked / hit / unhit / untracked counts for the analyzed
631 /// runtime coverage input.
632 pub summary: RuntimeCoverageSummary,
633 #[serde(default, skip_serializing_if = "Vec::is_empty")]
634 #[cfg_attr(feature = "schema", schemars(default))]
635 /// Surfaced runtime coverage findings (`safe_to_delete`, `review_required`,
636 /// `low_traffic`, `coverage_unavailable`). Omitted when empty. `active`
637 /// functions stay out of this list so the CLI output remains actionable.
638 pub findings: Vec<RuntimeCoverageFinding>,
639 #[serde(default, skip_serializing_if = "Vec::is_empty")]
640 #[cfg_attr(feature = "schema", schemars(default))]
641 /// Top runtime functions by invocation count. Omitted when empty.
642 pub hot_paths: Vec<RuntimeCoverageHotPath>,
643 /// First-class blast-radius entries for runtime-observed functions. Present
644 /// whenever runtime coverage analysis runs.
645 pub blast_radius: Vec<RuntimeCoverageBlastRadiusEntry>,
646 /// First-class production-importance entries for runtime-observed
647 /// functions. Present whenever runtime coverage analysis runs.
648 pub importance: Vec<RuntimeCoverageImportanceEntry>,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
650 /// License/trial watermark for grace-mode output. Omitted when not
651 /// applicable.
652 pub watermark: Option<RuntimeCoverageWatermark>,
653 #[serde(default, skip_serializing_if = "Vec::is_empty")]
654 #[cfg_attr(feature = "schema", schemars(default))]
655 /// Non-fatal merge or coverage diagnostics. Omitted when empty.
656 pub warnings: Vec<RuntimeCoverageMessage>,
657 /// Whether an autonomous agent may act on this report (fallow-rs/fallow-cloud#316,
658 /// mirrors the cloud runtime-context contract). `false` when the capture
659 /// carries no usable runtime evidence (no tracked functions); then
660 /// `actionability_verdict` is `insufficient_evidence` and
661 /// `actionability_reason` explains. F4: a non-action floor, never a gate on a
662 /// positive verdict.
663 pub actionable: bool,
664 /// Why the report is non-actionable; `null` when `actionable` is true.
665 #[serde(default, skip_serializing_if = "Option::is_none")]
666 #[cfg_attr(feature = "schema", schemars(default))]
667 pub actionability_reason: Option<String>,
668 /// First-class non-action verdict (`insufficient_evidence`) when not
669 /// actionable; `null` otherwise. Mirrors the cloud runtime-context `verdict`;
670 /// named distinctly from the report-context `verdict` above to avoid a
671 /// collision.
672 #[serde(default, skip_serializing_if = "Option::is_none")]
673 #[cfg_attr(feature = "schema", schemars(default))]
674 pub actionability_verdict: Option<String>,
675 /// Provenance an agent reads to self-attenuate confidence (fallow-rs/fallow-cloud#319,
676 /// mirrors the cloud runtime-context `provenance`). F4: context only.
677 pub provenance: RuntimeCoverageProvenance,
678}
679
680/// Provenance of a runtime-coverage report (fallow-rs/fallow-cloud#319), mirroring
681/// the cloud runtime-context `provenance` block so the local-capture and cloud
682/// surfaces present one portable shape. F4: provenance is context only; it never
683/// gates a verdict or confidence.
684#[derive(Debug, Clone, serde::Serialize)]
685#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
686pub struct RuntimeCoverageProvenance {
687 /// `local` for a local capture, `cloud` for the cloud read path.
688 pub data_source: RuntimeCoverageDataSource,
689 /// `true` / `false` / `unknown`. Always `unknown` for a local capture: the
690 /// local path has no deployment-origin signal (the cloud may resolve it).
691 pub is_production: String,
692 /// Age in whole days of the most recent evidence; `0` for a fresh local
693 /// capture, `null` when no runtime data is present.
694 #[serde(default, skip_serializing_if = "Option::is_none")]
695 #[cfg_attr(feature = "schema", schemars(default))]
696 pub freshness_days: Option<u32>,
697 /// `functions_untracked / (functions_tracked + functions_untracked)`, in
698 /// `[0, 1]`. High ratios mark a thin / partial capture.
699 pub untracked_ratio: f64,
700 /// Fraction of resolution-attempted functions whose position could not be
701 /// mapped to source, in `[0, 1]`. `0` for a local capture (positions resolve
702 /// natively or via the sidecar).
703 pub unresolved_ratio: f64,
704 /// Whether `freshness_days` exceeds `stale_after_days`.
705 pub stale: bool,
706 /// The documented staleness cutoff (days), echoed so the rule travels in-band.
707 pub stale_after_days: u32,
708}
709
710impl Default for RuntimeCoverageProvenance {
711 fn default() -> Self {
712 Self {
713 data_source: RuntimeCoverageDataSource::Local,
714 is_production: "unknown".to_owned(),
715 freshness_days: Some(0),
716 untracked_ratio: 0.0,
717 unresolved_ratio: 0.0,
718 stale: false,
719 stale_after_days: RUNTIME_STALE_AFTER_DAYS,
720 }
721 }
722}
723
724/// Staleness cutoff in days, mirrored from the cloud runtime-context
725/// (`RUNTIME_STALE_AFTER_DAYS`) so the local and cloud contracts agree.
726pub const RUNTIME_STALE_AFTER_DAYS: u32 = 14;
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
733 fn report_verdict_display_matches_kebab_case_serde() {
734 assert_eq!(RuntimeCoverageReportVerdict::Clean.to_string(), "clean");
735 assert_eq!(
736 RuntimeCoverageReportVerdict::HotPathTouched.to_string(),
737 "hot-path-touched",
738 );
739 assert_eq!(
740 RuntimeCoverageReportVerdict::ColdCodeDetected.to_string(),
741 "cold-code-detected",
742 );
743 assert_eq!(
744 RuntimeCoverageReportVerdict::LicenseExpiredGrace.to_string(),
745 "license-expired-grace",
746 );
747 assert_eq!(RuntimeCoverageReportVerdict::Unknown.to_string(), "unknown",);
748 }
749
750 #[test]
751 fn verdict_display_matches_snake_case_serde() {
752 assert_eq!(
753 RuntimeCoverageVerdict::SafeToDelete.to_string(),
754 "safe_to_delete",
755 );
756 assert_eq!(
757 RuntimeCoverageVerdict::ReviewRequired.to_string(),
758 "review_required",
759 );
760 assert_eq!(
761 RuntimeCoverageVerdict::CoverageUnavailable.to_string(),
762 "coverage_unavailable",
763 );
764 assert_eq!(
765 RuntimeCoverageVerdict::LowTraffic.to_string(),
766 "low_traffic",
767 );
768 assert_eq!(RuntimeCoverageVerdict::Active.to_string(), "active");
769 }
770
771 #[test]
772 fn confidence_display_matches_snake_case_serde() {
773 assert_eq!(RuntimeCoverageConfidence::VeryHigh.to_string(), "very_high",);
774 assert_eq!(RuntimeCoverageConfidence::High.to_string(), "high");
775 assert_eq!(RuntimeCoverageConfidence::Medium.to_string(), "medium");
776 assert_eq!(RuntimeCoverageConfidence::Low.to_string(), "low");
777 assert_eq!(RuntimeCoverageConfidence::None.to_string(), "none");
778 assert_eq!(RuntimeCoverageConfidence::Unknown.to_string(), "unknown");
779 }
780
781 #[test]
782 fn watermark_display_matches_kebab_case_serde() {
783 assert_eq!(
784 RuntimeCoverageWatermark::TrialExpired.to_string(),
785 "trial-expired",
786 );
787 assert_eq!(
788 RuntimeCoverageWatermark::LicenseExpiredGrace.to_string(),
789 "license-expired-grace",
790 );
791 }
792
793 #[test]
794 fn action_serializes_kind_as_type() {
795 let action = RuntimeCoverageAction {
796 kind: "review-deletion".to_owned(),
797 description: "Remove the function.".to_owned(),
798 auto_fixable: false,
799 };
800 let value = serde_json::to_value(&action).expect("action should serialize");
801 assert_eq!(value["type"], "review-deletion");
802 assert!(
803 value.get("kind").is_none(),
804 "kind should be renamed to type"
805 );
806 }
807}