Coverage Report

Created: 2026-10-28 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fallow/crates/output/src/doctor.rs
Line
Count
Source
1
//! Typed readiness report emitted by `fallow doctor`.
2
3
use crate::root_envelopes::serialize_named_json_output;
4
use fallow_types::envelope::{SchemaVersion, ToolVersion};
5
use serde::Serialize;
6
7
/// Current schema version for `fallow doctor --format json`.
8
///
9
/// Bumped to 2 for the `dependencies`, `cache`, and `graph-cache` checks. All
10
/// three are appended after the existing five, so the previous order is
11
/// unchanged, but a consumer that enumerates `checks[]` sees three new `id`
12
/// values.
13
pub const DOCTOR_SCHEMA_VERSION: u32 = 2;
14
15
/// Schema projection for the exact doctor envelope version.
16
#[cfg(feature = "schema")]
17
#[allow(dead_code, reason = "schema-only type used by the field projection")]
18
#[derive(schemars::JsonSchema)]
19
#[schemars(extend("const" = DOCTOR_SCHEMA_VERSION))]
20
struct DoctorSchemaVersion(u32);
21
22
/// Schema projection for `.` as the privacy-safe diagnosed project root.
23
#[cfg(feature = "schema")]
24
#[allow(dead_code, reason = "schema-only type used by the field projection")]
25
#[derive(schemars::JsonSchema)]
26
#[schemars(extend("const" = "."))]
27
struct DoctorProjectRoot(String);
28
29
/// Aggregate readiness outcome.
30
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32
#[serde(rename_all = "kebab-case")]
33
pub enum DoctorStatus {
34
    /// Every applicable check passed.
35
    Pass,
36
    /// Readiness is usable, with one or more advisory warnings.
37
    Warn,
38
    /// At least one required readiness check failed.
39
    Fail,
40
}
41
42
/// Per-check readiness outcome.
43
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
44
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45
#[serde(rename_all = "kebab-case")]
46
pub enum DoctorCheckStatus {
47
    /// The check completed successfully.
48
    Pass,
49
    /// The check completed with an advisory concern.
50
    Warn,
51
    /// The check found a blocking readiness problem.
52
    Fail,
53
    /// The check does not apply or an earlier prerequisite failed.
54
    Skipped,
55
}
56
57
/// Stable identifier for a doctor check. Declaration order is output order.
58
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
59
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60
#[serde(rename_all = "kebab-case")]
61
pub enum DoctorCheckId {
62
    /// Project root availability.
63
    Root,
64
    /// Fallow configuration resolution.
65
    Config,
66
    /// Workspace declaration discovery.
67
    Workspaces,
68
    /// External plugin configuration and activation.
69
    Plugins,
70
    /// Optional type-aware companion discovery.
71
    TypeAware,
72
    /// Installed dependency tree availability.
73
    Dependencies,
74
    /// Persisted extraction-cache reuse.
75
    Cache,
76
    /// Persisted module-graph reuse.
77
    ///
78
    /// Separate from [`Cache`](Self::Cache) because a warm run reuses the two
79
    /// blobs independently: the extraction cache can be perfectly reusable
80
    /// while the graph is discarded on every run, and the graph is the larger
81
    /// of the two on a real project.
82
    GraphCache,
83
}
84
85
/// Stable category for a doctor check.
86
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
87
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88
#[serde(rename_all = "kebab-case")]
89
pub enum DoctorCheckCategory {
90
    /// Project filesystem readiness.
91
    Project,
92
    /// Fallow configuration readiness.
93
    Configuration,
94
    /// Workspace topology readiness.
95
    Workspace,
96
    /// Plugin readiness.
97
    Plugin,
98
    /// Optional companion readiness.
99
    Companion,
100
    /// Warm-run readiness of persisted analysis state.
101
    Cache,
102
}
103
104
/// Actionable remediation attached to a doctor check.
105
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
106
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
107
pub struct DoctorRemediation {
108
    /// Command the user or agent can choose to run.
109
    pub command: String,
110
    /// Working directory for the command, relative to the diagnosed root.
111
    #[cfg_attr(feature = "schema", schemars(with = "DoctorProjectRoot"))]
112
    pub cwd: String,
113
    /// Whether running the command can modify project files or dependencies.
114
    pub mutating: bool,
115
}
116
117
/// One deterministic doctor check result.
118
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
119
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120
pub struct DoctorCheck {
121
    /// Stable check identifier.
122
    pub id: DoctorCheckId,
123
    /// Readiness area this check covers.
124
    pub category: DoctorCheckCategory,
125
    /// Check outcome.
126
    pub status: DoctorCheckStatus,
127
    /// Whether failure makes the project not ready.
128
    pub required: bool,
129
    /// Human-readable result with no host-specific absolute paths.
130
    pub message: String,
131
    /// Optional actionable next command.
132
    #[serde(default, skip_serializing_if = "Option::is_none")]
133
    pub remediation: Option<DoctorRemediation>,
134
}
135
136
/// Counts for every per-check status.
137
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
138
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
139
pub struct DoctorSummary {
140
    /// Successful checks.
141
    pub pass: usize,
142
    /// Advisory checks.
143
    pub warn: usize,
144
    /// Failed checks.
145
    pub fail: usize,
146
    /// Inapplicable or prerequisite-blocked checks.
147
    pub skipped: usize,
148
}
149
150
/// Versioned readiness envelope emitted by `fallow doctor --format json`.
151
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
152
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
153
#[cfg_attr(feature = "schema", schemars(title = "fallow doctor --format json"))]
154
pub struct DoctorOutput {
155
    /// Independent doctor contract version.
156
    #[cfg_attr(feature = "schema", schemars(with = "DoctorSchemaVersion"))]
157
    pub schema_version: SchemaVersion,
158
    /// Fallow version that produced the report.
159
    pub version: ToolVersion,
160
    /// Stable project-root identifier. Always `.` to avoid exposing host paths.
161
    #[cfg_attr(feature = "schema", schemars(with = "DoctorProjectRoot"))]
162
    pub root: String,
163
    /// Aggregate readiness outcome.
164
    pub status: DoctorStatus,
165
    /// Counts derived from `checks`.
166
    pub summary: DoctorSummary,
167
    /// Checks in stable contract order.
168
    pub checks: Vec<DoctorCheck>,
169
}
170
171
/// Serialize a doctor report with its root discriminator.
172
///
173
/// # Errors
174
///
175
/// Returns a serde error if the typed report cannot be converted to JSON.
176
0
pub fn serialize_doctor_json_output(
177
0
    output: DoctorOutput,
178
0
) -> Result<serde_json::Value, serde_json::Error> {
179
0
    serialize_named_json_output(output, "doctor")
180
0
}
181
182
#[cfg(test)]
183
mod tests {
184
    use super::*;
185
186
    #[test]
187
    fn tagged_json_uses_the_stable_privacy_safe_contract() {
188
        let value = serialize_doctor_json_output(DoctorOutput {
189
            schema_version: SchemaVersion(DOCTOR_SCHEMA_VERSION),
190
            version: ToolVersion("1.2.3".to_string()),
191
            root: ".".to_string(),
192
            status: DoctorStatus::Warn,
193
            summary: DoctorSummary {
194
                pass: 4,
195
                warn: 1,
196
                fail: 0,
197
                skipped: 0,
198
            },
199
            checks: vec![DoctorCheck {
200
                id: DoctorCheckId::TypeAware,
201
                category: DoctorCheckCategory::Companion,
202
                status: DoctorCheckStatus::Warn,
203
                required: false,
204
                message: "Companion is unavailable.".to_string(),
205
                remediation: Some(DoctorRemediation {
206
                    command: "npm install --save-dev fallow-type-aware@1.2.3".to_string(),
207
                    cwd: ".".to_string(),
208
                    mutating: true,
209
                }),
210
            }],
211
        })
212
        .expect("serialize doctor output");
213
214
        assert_eq!(value["kind"], "doctor");
215
        assert_eq!(value["schema_version"], DOCTOR_SCHEMA_VERSION);
216
        assert_eq!(value["root"], ".");
217
        assert_eq!(value["checks"][0]["remediation"]["cwd"], ".");
218
        assert_eq!(value["checks"][0]["remediation"]["mutating"], true);
219
        assert!(
220
            value["checks"][0]["remediation"]
221
                .get("mutates_project")
222
                .is_none()
223
        );
224
    }
225
}