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}
161
162/// Envelope emitted by `fallow coverage analyze --format json`.
163#[derive(Debug, Clone, Serialize)]
164#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
165#[cfg_attr(
166    feature = "schema",
167    schemars(title = "fallow coverage analyze --format json")
168)]
169pub struct CoverageAnalyzeOutput {
170    /// Analyze output schema version; serialized as the string `"1"`.
171    pub schema_version: CoverageAnalyzeSchemaVersion,
172    /// Fallow CLI version that produced this output.
173    pub version: ToolVersion,
174    /// Wall-clock analysis duration in milliseconds.
175    pub elapsed_ms: ElapsedMs,
176    /// Runtime-coverage report body.
177    pub runtime_coverage: RuntimeCoverageReport,
178    /// `_meta` block with docs and metric definitions, when `--explain` was
179    /// passed.
180    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
181    pub meta: Option<Meta>,
182}
183
184/// Serialize the `fallow coverage setup --json` envelope.
185///
186/// # Errors
187///
188/// Returns a serde error when the envelope cannot be converted to JSON.
189pub fn serialize_coverage_setup_json_output(
190    output: CoverageSetupOutput,
191    mode: RootEnvelopeMode,
192    analysis_run_id: Option<&str>,
193) -> Result<serde_json::Value, serde_json::Error> {
194    let mut value = serialize_named_json_output(output, "coverage-setup", mode)?;
195    attach_telemetry_meta(&mut value, analysis_run_id);
196    Ok(value)
197}
198
199/// Build the `fallow coverage analyze --format json` envelope.
200#[must_use]
201pub fn build_coverage_analyze_output(
202    report: &RuntimeCoverageReport,
203    elapsed: Duration,
204    version: impl Into<String>,
205) -> CoverageAnalyzeOutput {
206    CoverageAnalyzeOutput {
207        schema_version: CoverageAnalyzeSchemaVersion::V1,
208        version: ToolVersion(version.into()),
209        elapsed_ms: ElapsedMs(u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)),
210        runtime_coverage: report.clone(),
211        meta: None,
212    }
213}
214
215/// Serialize the `fallow coverage analyze --format json` envelope.
216///
217/// `explain_meta` is inserted after typed-envelope serialization because the
218/// existing command metadata is a JSON object shared with docs/schema helpers.
219///
220/// # Errors
221///
222/// Returns a serde error when the envelope cannot be converted to JSON.
223pub fn serialize_coverage_analyze_json_output(
224    output: CoverageAnalyzeOutput,
225    mode: RootEnvelopeMode,
226    explain_meta: Option<serde_json::Value>,
227    analysis_run_id: Option<&str>,
228) -> Result<serde_json::Value, serde_json::Error> {
229    let mut value = serialize_named_json_output(output, "coverage-analyze", mode)?;
230    if let Some(meta) = explain_meta
231        && let Some(map) = value.as_object_mut()
232    {
233        map.insert("_meta".to_owned(), meta);
234    }
235    attach_telemetry_meta(&mut value, analysis_run_id);
236    Ok(value)
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use serde_json::json;
243
244    #[test]
245    fn coverage_setup_json_output_uses_named_root_contract() {
246        let output = CoverageSetupOutput {
247            schema_version: CoverageSetupSchemaVersion::V1,
248            framework_detected: CoverageSetupFramework::Unknown,
249            package_manager: None,
250            runtime_targets: Vec::new(),
251            members: Vec::new(),
252            config_written: None,
253            commands: Vec::new(),
254            files_to_edit: Vec::new(),
255            snippets: Vec::new(),
256            dockerfile_snippet: None,
257            next_steps: Vec::new(),
258            warnings: Vec::new(),
259            meta: None,
260        };
261
262        let value =
263            serialize_coverage_setup_json_output(output, RootEnvelopeMode::Tagged, Some("run-1"))
264                .expect("coverage setup should serialize");
265
266        assert_eq!(value["kind"], "coverage-setup");
267        assert_eq!(value["schema_version"], "1");
268        assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-1");
269    }
270
271    #[test]
272    fn coverage_analyze_json_output_inserts_explain_meta_and_telemetry() {
273        let report = RuntimeCoverageReport::default();
274        let output = build_coverage_analyze_output(&report, Duration::from_millis(7), "test");
275
276        let value = serialize_coverage_analyze_json_output(
277            output,
278            RootEnvelopeMode::Tagged,
279            Some(json!({"docs": "coverage"})),
280            Some("run-2"),
281        )
282        .expect("coverage analyze should serialize");
283
284        assert_eq!(value["kind"], "coverage-analyze");
285        assert_eq!(value["schema_version"], "1");
286        assert_eq!(value["elapsed_ms"], 7);
287        assert_eq!(value["_meta"]["docs"], "coverage");
288        assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-2");
289    }
290}