/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 | | } |