Skip to main content

fallow_engine/
flag_report.rs

1//! One entry point that builds the `fallow flags --retirement` report.
2//!
3//! The CLI and the programmatic API (and through it the MCP server) call this
4//! function, so the two surfaces run the same steps in the same order: group
5//! the sites into rows, measure the age, apply the vendor export, compute the
6//! age gate, then filter, sort and limit the rows.
7
8use std::path::Path;
9
10use fallow_config::WorkspaceInfo;
11use fallow_types::flag_retirement::{FlagAgeMode, FlagRetirementReport, RetirementFlagKind};
12use fallow_types::workspace::WorkspaceDiagnosticKind;
13use rustc_hash::FxHashSet;
14
15use crate::clock::AnalysisClock;
16use crate::flag_age::{FlagAgeRequest, PickaxeProgress, apply_flag_ages};
17use crate::flag_retirement::{
18    AGE_GATE_AGE_OFF, AGE_GATE_NO_HISTORY, RetirementOptions, RetirementSiteInput, aggregate_flags,
19    finish_report, max_age_gate,
20};
21use crate::flag_vendor::{VendorExport, VendorMatch, apply_vendor_state};
22
23/// Inputs of one retirement report.
24pub struct RetirementRequest<'a> {
25    /// Project root. Paths in the report are relative to it.
26    pub root: &'a Path,
27    /// Workspaces of the project, for the flag identity.
28    pub workspaces: &'a [WorkspaceInfo],
29    /// Every flag site of the project, also outside the scope of the run.
30    pub sites: Vec<RetirementSiteInput>,
31    /// Whether a file is in the scope of the run.
32    pub in_scope: &'a dyn Fn(&Path) -> bool,
33    /// Whether the run covers the whole project. Only such a run adds
34    /// `vendor-only` rows.
35    pub whole_project: bool,
36    /// How to measure flag age.
37    pub age_mode: FlagAgeMode,
38    /// Directory for the age cache, or `None` to run without the cache.
39    pub cache_dir: Option<&'a Path>,
40    /// Receives pickaxe progress.
41    pub progress: Option<&'a (dyn Fn(PickaxeProgress) + Sync)>,
42    /// The `--flag-state` export, if any.
43    pub vendor_export: Option<&'a VendorExport>,
44    /// `flags.vendorKeyPrefix`.
45    pub vendor_key_prefix: Option<&'a str>,
46    /// `--max-flag-age`, in days.
47    pub max_flag_age: Option<u64>,
48    /// Filters, order and limit of the rows.
49    pub options: RetirementOptions,
50}
51
52/// A retirement report and the reasons that ages are missing.
53pub struct RetirementBuild {
54    /// The report.
55    pub report: FlagRetirementReport,
56    /// Why no age was measured, when git history was not available.
57    pub diagnostics: Vec<WorkspaceDiagnosticKind>,
58}
59
60/// Build the retirement report.
61#[must_use]
62pub fn build_retirement_report(request: RetirementRequest<'_>) -> RetirementBuild {
63    let root = request.root;
64    let code_flag_names: FxHashSet<String> = request
65        .sites
66        .iter()
67        .map(|site| site.flag_name.clone())
68        .collect();
69    let project_sdk_labels: FxHashSet<String> = request
70        .sites
71        .iter()
72        .filter(|site| site.kind == RetirementFlagKind::SdkCall)
73        .filter_map(|site| site.sdk_name.clone())
74        .collect();
75    let mut rows = aggregate_flags(request.sites, root, request.workspaces, request.in_scope);
76    let age = apply_flag_ages(
77        &mut rows,
78        &FlagAgeRequest {
79            root,
80            mode: request.age_mode,
81            cache_dir: request.cache_dir,
82            progress: request.progress,
83        },
84    );
85    let vendor_state = request.vendor_export.map(|export| {
86        apply_vendor_state(
87            &mut rows,
88            &VendorMatch {
89                export,
90                key_prefix: request.vendor_key_prefix,
91                code_flag_names: &code_flag_names,
92                project_sdk_labels: &project_sdk_labels,
93                add_vendor_only: request.whole_project,
94                clock_epoch_secs: AnalysisClock::for_repo(root).epoch_secs(),
95            },
96        )
97    });
98    let skip_reason = if request.age_mode == FlagAgeMode::Off {
99        Some(AGE_GATE_AGE_OFF)
100    } else if age.generated_at_clock.is_none() {
101        Some(AGE_GATE_NO_HISTORY)
102    } else {
103        None
104    };
105    let max_flag_age = request
106        .max_flag_age
107        .map(|days| max_age_gate(&rows, days, skip_reason));
108    let mut report = finish_report(
109        rows,
110        request.age_mode,
111        age.generated_at_clock,
112        &request.options,
113    );
114    report.vendor_state = vendor_state;
115    report.max_flag_age = max_flag_age;
116    RetirementBuild {
117        report,
118        diagnostics: age.diagnostics,
119    }
120}