Skip to main content

fallow_output/
doctor.rs

1//! Typed readiness report emitted by `fallow doctor`.
2
3use crate::root_envelopes::serialize_named_json_output;
4use fallow_types::envelope::{SchemaVersion, ToolVersion};
5use 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.
13pub 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))]
20struct 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" = "."))]
27struct 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")]
33pub 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")]
46pub 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")]
61pub 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")]
89pub 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))]
107pub 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))]
120pub 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))]
139pub 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"))]
154pub 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.
176pub fn serialize_doctor_json_output(
177    output: DoctorOutput,
178) -> Result<serde_json::Value, serde_json::Error> {
179    serialize_named_json_output(output, "doctor")
180}
181
182#[cfg(test)]
183mod 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}