Skip to main content

fallow_output/
coverage_envelopes.rs

1//! Coverage command output envelopes.
2
3use crate::RuntimeCoverageReport;
4use crate::root_envelopes::{RootEnvelopeMode, attach_telemetry_meta, serialize_named_json_output};
5use fallow_types::envelope::{ElapsedMs, Meta, ToolVersion};
6use serde::Serialize;
7use std::time::Duration;
8
9/// `fallow coverage setup --json` envelope.
10#[derive(Debug, Clone, Serialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[cfg_attr(feature = "schema", schemars(title = "fallow coverage setup --json"))]
13pub struct CoverageSetupOutput {
14    /// Setup output schema version; serialized as the string `"1"`.
15    pub schema_version: CoverageSetupSchemaVersion,
16    /// Framework detected at the project root.
17    pub framework_detected: CoverageSetupFramework,
18    /// Package manager detected from lockfiles, when one was found.
19    pub package_manager: Option<CoverageSetupPackageManager>,
20    /// Runtimes the instrumentation must cover at the project root.
21    pub runtime_targets: Vec<CoverageSetupRuntimeTarget>,
22    /// Per-member setup guidance for workspace projects.
23    pub members: Vec<CoverageSetupMember>,
24    /// Coverage config that was written to disk, when setup wrote one.
25    pub config_written: Option<serde_json::Value>,
26    /// Shell commands the user should run to complete setup.
27    pub commands: Vec<String>,
28    /// Files the user must edit by hand, with reasons.
29    pub files_to_edit: Vec<CoverageSetupFileToEdit>,
30    /// Ready-to-paste code snippets for the files to edit.
31    pub snippets: Vec<CoverageSetupSnippet>,
32    /// Dockerfile additions needed for containerized capture, when relevant.
33    pub dockerfile_snippet: Option<String>,
34    /// Ordered human-readable follow-up instructions.
35    pub next_steps: Vec<String>,
36    /// Non-fatal problems encountered during detection.
37    pub warnings: Vec<String>,
38    /// `_meta` block with docs and field definitions, when requested.
39    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
40    pub meta: Option<serde_json::Value>,
41}
42
43/// Schema-version discriminator for [`CoverageSetupOutput`].
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46pub enum CoverageSetupSchemaVersion {
47    /// First release of the coverage setup format.
48    #[serde(rename = "1")]
49    V1,
50}
51
52/// Framework detected during coverage setup; drives which instrumentation
53/// guidance is emitted.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
55#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
56#[serde(rename_all = "snake_case")]
57pub enum CoverageSetupFramework {
58    /// Next.js application.
59    #[serde(rename = "nextjs")]
60    NextJs,
61    /// NestJS application.
62    #[serde(rename = "nestjs")]
63    NestJs,
64    /// Nuxt application.
65    Nuxt,
66    /// SvelteKit application.
67    #[serde(rename = "sveltekit")]
68    SvelteKit,
69    /// Astro application.
70    Astro,
71    /// Remix application.
72    Remix,
73    /// Vite-built application without a detected meta-framework.
74    Vite,
75    /// Node project without a detected framework or bundler.
76    PlainNode,
77    /// No framework signal was found.
78    Unknown,
79}
80
81/// Package manager detected from the project's lockfile.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[serde(rename_all = "lowercase")]
85pub enum CoverageSetupPackageManager {
86    /// npm (`package-lock.json`).
87    Npm,
88    /// pnpm (`pnpm-lock.yaml`).
89    Pnpm,
90    /// Yarn (`yarn.lock`).
91    Yarn,
92    /// Bun (`bun.lock` / `bun.lockb`).
93    Bun,
94}
95
96/// Runtime environment coverage capture must instrument.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
98#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
99#[serde(rename_all = "lowercase")]
100pub enum CoverageSetupRuntimeTarget {
101    /// Server-side Node.js execution.
102    Node,
103    /// Client-side browser execution.
104    Browser,
105}
106
107/// Per-workspace-member setup guidance inside [`CoverageSetupOutput::members`].
108#[derive(Debug, Clone, Serialize)]
109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110pub struct CoverageSetupMember {
111    /// Package name of the workspace member.
112    pub name: String,
113    /// Member path relative to the workspace root.
114    pub path: String,
115    /// Framework detected for this member.
116    pub framework_detected: CoverageSetupFramework,
117    /// Package manager detected for this member, when one was found.
118    pub package_manager: Option<CoverageSetupPackageManager>,
119    /// Runtimes the instrumentation must cover for this member.
120    pub runtime_targets: Vec<CoverageSetupRuntimeTarget>,
121    /// Files the user must edit by hand, with reasons.
122    pub files_to_edit: Vec<CoverageSetupFileToEdit>,
123    /// Ready-to-paste code snippets for the files to edit.
124    pub snippets: Vec<CoverageSetupSnippet>,
125    /// Dockerfile additions needed for containerized capture, when relevant.
126    pub dockerfile_snippet: Option<String>,
127    /// Non-fatal problems encountered during detection.
128    pub warnings: Vec<String>,
129}
130
131/// One manual edit the user must make to wire up coverage capture.
132#[derive(Debug, Clone, Serialize)]
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
134pub struct CoverageSetupFileToEdit {
135    /// File path relative to the project root.
136    pub path: String,
137    /// Why the file needs editing.
138    pub reason: String,
139}
140
141/// Ready-to-paste code snippet accompanying a file edit.
142#[derive(Debug, Clone, Serialize)]
143#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
144pub struct CoverageSetupSnippet {
145    /// Short description of what the snippet does.
146    pub label: String,
147    /// File path the snippet belongs in.
148    pub path: String,
149    /// The snippet source text.
150    pub content: String,
151}
152
153/// Schema-version discriminator for [`CoverageAnalyzeOutput`].
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
155#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
156pub enum CoverageAnalyzeSchemaVersion {
157    /// First release of the coverage analyze format.
158    #[serde(rename = "1")]
159    V1,
160    /// Expands the required semantic omission reason-code enum.
161    #[serde(rename = "2")]
162    V2,
163}
164
165/// Envelope emitted by `fallow coverage analyze --format json`.
166#[derive(Debug, Clone, Serialize)]
167#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
168#[cfg_attr(
169    feature = "schema",
170    schemars(title = "fallow coverage analyze --format json")
171)]
172pub struct CoverageAnalyzeOutput {
173    /// Analyze output schema version; currently serialized as the string `"2"`.
174    pub schema_version: CoverageAnalyzeSchemaVersion,
175    /// Fallow CLI version that produced this output.
176    pub version: ToolVersion,
177    /// Wall-clock analysis duration in milliseconds.
178    pub elapsed_ms: ElapsedMs,
179    /// Runtime-coverage report body.
180    pub runtime_coverage: RuntimeCoverageReport,
181    /// `_meta` block with docs and metric definitions, when `--explain` was
182    /// passed.
183    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
184    pub meta: Option<Meta>,
185}
186
187/// Serialize the `fallow coverage setup --json` envelope.
188///
189/// # Errors
190///
191/// Returns a serde error when the envelope cannot be converted to JSON.
192pub fn serialize_coverage_setup_json_output(
193    output: CoverageSetupOutput,
194    mode: RootEnvelopeMode,
195    analysis_run_id: Option<&str>,
196) -> Result<serde_json::Value, serde_json::Error> {
197    let mut value = serialize_named_json_output(output, "coverage-setup", mode)?;
198    attach_telemetry_meta(&mut value, analysis_run_id);
199    Ok(value)
200}
201
202/// Build the `fallow coverage analyze --format json` envelope.
203#[must_use]
204pub fn build_coverage_analyze_output(
205    report: &RuntimeCoverageReport,
206    elapsed: Duration,
207    version: impl Into<String>,
208) -> CoverageAnalyzeOutput {
209    CoverageAnalyzeOutput {
210        schema_version: CoverageAnalyzeSchemaVersion::V2,
211        version: ToolVersion(version.into()),
212        elapsed_ms: ElapsedMs(u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)),
213        runtime_coverage: report.clone(),
214        meta: None,
215    }
216}
217
218/// Serialize the `fallow coverage analyze --format json` envelope.
219///
220/// `explain_meta` is inserted after typed-envelope serialization because the
221/// existing command metadata is a JSON object shared with docs/schema helpers.
222///
223/// # Errors
224///
225/// Returns a serde error when the envelope cannot be converted to JSON.
226pub fn serialize_coverage_analyze_json_output(
227    output: CoverageAnalyzeOutput,
228    mode: RootEnvelopeMode,
229    explain_meta: Option<serde_json::Value>,
230    analysis_run_id: Option<&str>,
231) -> Result<serde_json::Value, serde_json::Error> {
232    let mut value = serialize_named_json_output(output, "coverage-analyze", mode)?;
233    if let Some(meta) = explain_meta
234        && let Some(map) = value.as_object_mut()
235    {
236        map.insert("_meta".to_owned(), meta);
237    }
238    attach_telemetry_meta(&mut value, analysis_run_id);
239    Ok(value)
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use serde_json::json;
246
247    #[test]
248    fn coverage_setup_json_output_uses_named_root_contract() {
249        let output = CoverageSetupOutput {
250            schema_version: CoverageSetupSchemaVersion::V1,
251            framework_detected: CoverageSetupFramework::Unknown,
252            package_manager: None,
253            runtime_targets: Vec::new(),
254            members: Vec::new(),
255            config_written: None,
256            commands: Vec::new(),
257            files_to_edit: Vec::new(),
258            snippets: Vec::new(),
259            dockerfile_snippet: None,
260            next_steps: Vec::new(),
261            warnings: Vec::new(),
262            meta: None,
263        };
264
265        let value =
266            serialize_coverage_setup_json_output(output, RootEnvelopeMode::Tagged, Some("run-1"))
267                .expect("coverage setup should serialize");
268
269        assert_eq!(value["kind"], "coverage-setup");
270        assert_eq!(value["schema_version"], "1");
271        assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-1");
272    }
273
274    #[test]
275    fn coverage_analyze_json_output_inserts_explain_meta_and_telemetry() {
276        let report = RuntimeCoverageReport::default();
277        let output = build_coverage_analyze_output(&report, Duration::from_millis(7), "test");
278
279        let value = serialize_coverage_analyze_json_output(
280            output,
281            RootEnvelopeMode::Tagged,
282            Some(json!({"docs": "coverage"})),
283            Some("run-2"),
284        )
285        .expect("coverage analyze should serialize");
286
287        assert_eq!(value["kind"], "coverage-analyze");
288        assert_eq!(value["schema_version"], "2");
289        assert_eq!(value["elapsed_ms"], 7);
290        assert_eq!(value["_meta"]["docs"], "coverage");
291        assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-2");
292    }
293}