Skip to main content

supercov_engine/
coverage_report.rs

1//! Language-neutral reconstruction of coverage views from frozen obligations
2//! and per-attempt evidence.
3//!
4//! This module deliberately knows nothing about JavaScript or any test runner.
5//! Language frontends provide the manifest and normalized evidence records;
6//! Rust owns merging, attempt outcomes, attribution confidence, filtering and
7//! every structural coverage verdict.
8
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use supercov_contracts::{COVERAGE_MODEL_SCHEMA_VERSION, FrontendRunDeclaration};
15
16use crate::coverage_analysis::{
17    AnalysisError, BranchCoverage, CoverageCoreInput, CoverageSummary, DecisionCoverage,
18    McdcVector, PointCoverage, PointKind, analyze_core, find_witnesses_for_conditions,
19};
20use crate::evidence_archive::{EvidenceArchiveEntry, read_archive};
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24pub struct DecisionMeta {
25    pub id: String,
26    pub file: String,
27    pub line: usize,
28    pub column: usize,
29    pub source: String,
30    pub conditions: Vec<String>,
31    pub kind: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct PointMeta {
37    pub id: String,
38    pub kind: PointKind,
39    pub file: String,
40    pub line: usize,
41    pub column: usize,
42    pub source: String,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub label: Option<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase", deny_unknown_fields)]
49pub struct BranchAlternativeMeta {
50    pub id: String,
51    pub label: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase", deny_unknown_fields)]
56pub struct BranchMeta {
57    pub id: String,
58    pub kind: String,
59    pub file: String,
60    pub line: usize,
61    pub column: usize,
62    pub source: String,
63    pub alternatives: Vec<BranchAlternativeMeta>,
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68pub struct CoverageManifest {
69    pub decisions: Vec<DecisionMeta>,
70    pub points: Vec<PointMeta>,
71    pub branches: Vec<BranchMeta>,
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub limitations: Vec<Value>,
74    /// Obligation IDs the frontend declined to measure exactly. They stay in
75    /// the manifest so the report can say what was not measured, but they must
76    /// never be counted as uncovered.
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub unmeasured: Vec<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub scope: Option<Value>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85pub struct DecisionSnapshot {
86    pub meta: DecisionMeta,
87    pub vectors: Vec<McdcVector>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct RuntimeEvent {
93    #[serde(rename = "type")]
94    pub event_type: String,
95    pub id: String,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub vector: Option<McdcVector>,
98    pub timestamp_ms: i64,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub phase_id: Option<String>,
101    pub environment: String,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
105#[serde(rename_all = "camelCase", deny_unknown_fields)]
106pub struct RuntimeSnapshot {
107    #[serde(default)]
108    pub decisions: Vec<DecisionSnapshot>,
109    #[serde(default)]
110    pub hits: Vec<String>,
111    #[serde(default)]
112    pub events: Vec<RuntimeEvent>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117pub struct ExecutionScope {
118    pub version: usize,
119    pub run_id: String,
120    pub worker_id: String,
121    pub test_id: String,
122    pub test_key: String,
123    pub retry: usize,
124    pub attempt_id: String,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase", deny_unknown_fields)]
129pub struct ServerRecord {
130    #[serde(rename = "type")]
131    pub record_type: String,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub meta: Option<DecisionMeta>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub vector: Option<McdcVector>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub id: Option<String>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub timestamp_ms: Option<i64>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub phase_id: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub scope: Option<ExecutionScope>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct CoveragePhase {
149    pub id: String,
150    pub kind: String,
151    pub operation: String,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub source: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub caused_by_phase_id: Option<String>,
156    pub started_at_ms: i64,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub ended_at_ms: Option<i64>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub status: Option<String>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub error: Option<String>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase", deny_unknown_fields)]
167pub struct TestProvenance {
168    pub runner: String,
169    pub kind: String,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub project: Option<String>,
172    pub source: String,
173}
174
175impl Default for TestProvenance {
176    fn default() -> Self {
177        Self {
178            runner: "unknown".into(),
179            kind: "unknown".into(),
180            project: None,
181            source: "unknown".into(),
182        }
183    }
184}
185
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase", deny_unknown_fields)]
188pub struct RawTestResult {
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub test_id: Option<String>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub scope: Option<ExecutionScope>,
193    pub test: String,
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub test_file: Option<String>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub title: Option<String>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub retry: Option<usize>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub status: Option<String>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub expected_status: Option<String>,
204    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
205    pub flaky: bool,
206    #[serde(default)]
207    pub provenance: TestProvenance,
208    #[serde(default = "default_test_role")]
209    pub role: String,
210    #[serde(default)]
211    pub phases: Vec<CoveragePhase>,
212    #[serde(default)]
213    pub runtime: Vec<RuntimeSnapshot>,
214    #[serde(default)]
215    pub browser: Vec<RuntimeSnapshot>,
216    #[serde(default)]
217    pub server: Vec<ServerRecord>,
218}
219
220fn default_test_role() -> String {
221    "test".into()
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
225#[serde(rename_all = "camelCase")]
226pub struct TestAttempt {
227    pub retry: usize,
228    pub status: String,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub expected_status: Option<String>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
234#[serde(rename_all = "camelCase")]
235pub struct CoverageConfidence {
236    pub level: String,
237    pub setup_only: bool,
238    pub background_only: bool,
239    pub asserted: bool,
240    pub tests: Vec<String>,
241    pub asserted_tests: Vec<String>,
242    pub runners: Vec<String>,
243    pub kinds: Vec<String>,
244    pub e2e: bool,
245}
246
247#[derive(Debug, Clone, PartialEq, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct VectorObservation {
250    pub vector: McdcVector,
251    pub tests: Vec<String>,
252    #[serde(skip_serializing_if = "Vec::is_empty")]
253    pub phases: Vec<String>,
254    #[serde(skip_serializing_if = "Vec::is_empty")]
255    pub explicit_phases: Vec<String>,
256    pub confidence: CoverageConfidence,
257}
258
259#[derive(Debug, Clone, PartialEq, Serialize)]
260#[serde(rename_all = "camelCase")]
261pub struct ConditionResult {
262    pub index: usize,
263    pub source: String,
264    pub covered: bool,
265    pub assertion_covered: bool,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub witness: Option<[McdcVector; 2]>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub witness_tests: Option<[Vec<String>; 2]>,
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct DecisionResult {
275    pub meta: DecisionMeta,
276    pub executed: bool,
277    pub covered: bool,
278    pub vectors: Vec<McdcVector>,
279    pub vector_observations: Vec<VectorObservation>,
280    pub conditions: Vec<ConditionResult>,
281    pub tests: Vec<String>,
282    pub confidence: CoverageConfidence,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize)]
286#[serde(rename_all = "camelCase")]
287pub struct PointResult {
288    pub meta: PointMeta,
289    pub covered: bool,
290    pub tests: Vec<String>,
291    pub phases: Vec<String>,
292    pub confidence: CoverageConfidence,
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct AlternativeResult {
298    pub id: String,
299    pub label: String,
300    pub covered: bool,
301    pub tests: Vec<String>,
302    pub phases: Vec<String>,
303    pub confidence: CoverageConfidence,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize)]
307#[serde(rename_all = "camelCase")]
308pub struct BranchResult {
309    pub meta: BranchMeta,
310    pub covered: bool,
311    pub alternatives: Vec<AlternativeResult>,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
315pub struct SourceLine {
316    pub file: String,
317    pub line: usize,
318}
319
320/// One line's share of the points that sit on it, while the view is built.
321#[derive(Debug, Default)]
322struct LineAggregate {
323    covered: bool,
324    measured: bool,
325    tests: BTreeSet<String>,
326    phases: BTreeSet<String>,
327    explicit_phases: BTreeSet<String>,
328}
329
330#[derive(Debug, Clone, PartialEq, Serialize)]
331#[serde(rename_all = "camelCase")]
332pub struct LineResult {
333    pub file: String,
334    pub line: usize,
335    pub covered: bool,
336    /// False when every obligation on this line was declined, which keeps the
337    /// line addressable while leaving it out of the covered/total counts. Not
338    /// serialized: it decides a total, it is not a fact about the line worth
339    /// publishing, and the limitation records already say why.
340    #[serde(skip)]
341    pub measured: bool,
342    pub tests: Vec<String>,
343    pub runners: Vec<String>,
344    pub kinds: Vec<String>,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub exclusive_kind: Option<String>,
347    pub phases: Vec<String>,
348    pub confidence: CoverageConfidence,
349}
350
351#[derive(Debug, Clone, PartialEq, Serialize)]
352#[serde(rename_all = "camelCase")]
353pub struct TestDecisionResult {
354    pub id: String,
355    pub vectors: Vec<McdcVector>,
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct TestCoverageResult {
361    pub id: String,
362    pub name: String,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub file: Option<String>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub title: Option<String>,
367    pub retries: Vec<usize>,
368    pub attempts: Vec<TestAttempt>,
369    pub outcome: String,
370    pub provenance: TestProvenance,
371    pub role: String,
372    pub hits: Vec<String>,
373    pub decisions: Vec<TestDecisionResult>,
374    pub lines: Vec<SourceLine>,
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize)]
378#[serde(rename_all = "camelCase")]
379pub struct TestFileResult {
380    pub file: String,
381    pub tests: Vec<String>,
382    pub runners: Vec<String>,
383    pub kinds: Vec<String>,
384    pub lines: Vec<SourceLine>,
385}
386
387#[derive(Debug, Clone, PartialEq, Serialize)]
388#[serde(rename_all = "camelCase")]
389pub struct PhaseResult {
390    #[serde(flatten)]
391    pub phase: CoveragePhase,
392    pub test: String,
393    pub hits: Vec<String>,
394    pub decisions: Vec<TestDecisionResult>,
395    pub lines: Vec<SourceLine>,
396    pub browser_events: usize,
397    pub server_events: usize,
398    pub explicit_events: usize,
399    pub inferred_events: usize,
400    pub explicit_browser_events: usize,
401    pub inferred_browser_events: usize,
402    pub explicit_server_events: usize,
403    pub inferred_server_events: usize,
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize)]
407#[serde(rename_all = "camelCase")]
408pub struct DimensionCoverage {
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub kind: Option<String>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub runner: Option<String>,
413    pub tests: usize,
414    pub setups: usize,
415    pub summary: CoverageSummary,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
419#[serde(rename_all = "camelCase")]
420pub struct CoverageModel {
421    pub language: String,
422    pub name: String,
423    pub completeness_meaning: String,
424    pub measured: Vec<String>,
425    pub not_measured: Vec<String>,
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase", deny_unknown_fields)]
430pub struct CoverageModelDeclaration {
431    pub language: String,
432    pub variant: String,
433    pub name: String,
434    pub completeness_meaning: String,
435    pub measured: Vec<String>,
436    pub not_measured: Vec<String>,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase", deny_unknown_fields)]
441pub struct PersistedCoverageModel {
442    pub schema_version: u32,
443    pub language: String,
444    pub variant: String,
445    pub name: String,
446    pub completeness_meaning: String,
447    pub measured: Vec<String>,
448    pub not_measured: Vec<String>,
449}
450
451impl PersistedCoverageModel {
452    pub fn from_declaration(value: &CoverageModelDeclaration) -> Result<Self, &'static str> {
453        let persisted = Self {
454            schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
455            language: value.language.clone(),
456            variant: value.variant.clone(),
457            name: value.name.clone(),
458            completeness_meaning: value.completeness_meaning.clone(),
459            measured: value.measured.clone(),
460            not_measured: value.not_measured.clone(),
461        };
462        persisted.clone().into_declaration()?;
463        Ok(persisted)
464    }
465
466    fn into_declaration(self) -> Result<CoverageModelDeclaration, &'static str> {
467        if self.schema_version != COVERAGE_MODEL_SCHEMA_VERSION {
468            return Err("unsupported coverage model schema");
469        }
470        let valid_identifier = |value: &str| {
471            (1..=supercov_contracts::COVERAGE_MODEL_MAX_IDENTIFIER_BYTES).contains(&value.len())
472                && value.as_bytes()[0].is_ascii_lowercase()
473                && value.bytes().all(|byte| {
474                    byte.is_ascii_lowercase()
475                        || byte.is_ascii_digit()
476                        || matches!(byte, b'.' | b'+' | b'-')
477                })
478        };
479        let valid_description = |value: &str| {
480            !value.is_empty()
481                && value.trim().len() == value.len()
482                && value.len() <= supercov_contracts::COVERAGE_MODEL_MAX_DESCRIPTION_BYTES
483                && !value.chars().any(char::is_control)
484        };
485        if !valid_identifier(&self.language)
486            || !valid_identifier(&self.variant)
487            || !valid_description(&self.name)
488            || !valid_description(&self.completeness_meaning)
489            || self.measured.is_empty()
490            || self.measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
491            || self.not_measured.len() > supercov_contracts::COVERAGE_MODEL_MAX_SURFACES_PER_LIST
492            || self
493                .measured
494                .iter()
495                .chain(&self.not_measured)
496                .any(|item| !valid_description(item))
497        {
498            return Err("invalid coverage model declaration");
499        }
500        let measured = self.measured.iter().collect::<BTreeSet<_>>();
501        let not_measured = self.not_measured.iter().collect::<BTreeSet<_>>();
502        if measured.len() != self.measured.len()
503            || not_measured.len() != self.not_measured.len()
504            || !measured.is_disjoint(&not_measured)
505        {
506            return Err("invalid coverage model surface partition");
507        }
508        Ok(CoverageModelDeclaration {
509            language: self.language,
510            variant: self.variant,
511            name: self.name,
512            completeness_meaning: self.completeness_meaning,
513            measured: self.measured,
514            not_measured: self.not_measured,
515        })
516    }
517}
518
519#[derive(Debug, Clone, PartialEq, Serialize)]
520#[serde(rename_all = "camelCase")]
521pub struct CoverageView {
522    pub generated_at: String,
523    pub variant: String,
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub scope: Option<Value>,
526    pub model: CoverageModel,
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub integrity: Option<Value>,
529    pub limitations: Vec<Value>,
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub transport: Option<TransportStats>,
532    pub summary: CoverageSummary,
533    pub coverage_by_kind: Vec<DimensionCoverage>,
534    pub coverage_by_runner: Vec<DimensionCoverage>,
535    pub decisions: Vec<DecisionResult>,
536    pub points: Vec<PointResult>,
537    pub branches: Vec<BranchResult>,
538    pub tests: Vec<TestCoverageResult>,
539    pub test_files: Vec<TestFileResult>,
540    pub phases: Vec<PhaseResult>,
541    pub lines: Vec<LineResult>,
542}
543
544#[derive(Debug, Clone, PartialEq, Serialize)]
545#[serde(rename_all = "camelCase")]
546pub struct CoverageFilters {
547    pub passed: CoverageView,
548    pub failed: CoverageView,
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize)]
552#[serde(rename_all = "camelCase")]
553pub struct CoverageReport {
554    #[serde(flatten)]
555    pub view: CoverageView,
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub execution: Option<ExecutionResult>,
558    pub filters: CoverageFilters,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
562#[serde(rename_all = "camelCase")]
563pub struct ExecutionResult {
564    pub test_exit_code: Option<i32>,
565    pub valid: bool,
566}
567
568#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
569#[serde(rename_all = "camelCase")]
570pub struct TransportStats {
571    pub processes: usize,
572    pub child_launches: usize,
573    pub remote_launches: usize,
574    pub workspace_capabilities: usize,
575    pub scoped_server_records: usize,
576    pub background_server_records: usize,
577    pub corrupt_records: usize,
578    pub corrupt_files: usize,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
582#[serde(untagged)]
583enum ExecutionSafeArgument {
584    Text(String),
585    Digest(ExecutionArgumentDigest),
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
589#[serde(rename_all = "camelCase", deny_unknown_fields)]
590struct ExecutionArgumentDigest {
591    bytes: usize,
592    sha256: String,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
596#[serde(rename_all = "camelCase", deny_unknown_fields)]
597struct ExecutionCommandSummary {
598    #[serde(default)]
599    executable: Option<ExecutionSafeArgument>,
600    arguments: Vec<ExecutionSafeArgument>,
601    argument_count: usize,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
605#[serde(
606    tag = "event",
607    rename_all = "kebab-case",
608    rename_all_fields = "camelCase",
609    deny_unknown_fields
610)]
611enum ExecutionTraceEvent {
612    Process {
613        at: String,
614        pid: u32,
615        ppid: u32,
616        cwd: String,
617        command: ExecutionCommandSummary,
618        #[serde(default)]
619        entrypoint: Option<String>,
620    },
621    ChildLaunch {
622        at: String,
623        pid: u32,
624        ppid: u32,
625        method: String,
626        command: ExecutionSafeArgument,
627    },
628    RemoteLaunch {
629        at: String,
630        pid: u32,
631        ppid: u32,
632        command: ExecutionCommandSummary,
633        guest_root: String,
634    },
635    WorkspaceCapability {
636        at: String,
637        pid: u32,
638        ppid: u32,
639        host_root: String,
640        guest_root: String,
641        cache_identities: Vec<String>,
642    },
643}
644
645impl ExecutionTraceEvent {
646    fn kind(&self) -> &'static str {
647        match self {
648            Self::Process { .. } => "process",
649            Self::ChildLaunch { .. } => "child-launch",
650            Self::RemoteLaunch { .. } => "remote-launch",
651            Self::WorkspaceCapability { .. } => "workspace-capability",
652        }
653    }
654}
655
656#[derive(Debug, Clone, PartialEq, Deserialize)]
657#[serde(rename_all = "camelCase", deny_unknown_fields)]
658pub struct CoverageReportRequest {
659    pub run_id: String,
660    pub manifest: CoverageManifest,
661    pub raw_results: Vec<RawTestResult>,
662    pub generated_at: String,
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    pub coverage_model: Option<CoverageModelDeclaration>,
665    #[serde(skip_serializing_if = "Option::is_none")]
666    pub integrity: Option<Value>,
667    #[serde(default, deserialize_with = "deserialize_exit_code")]
668    pub test_exit_code: ExitCodeInput,
669}
670
671#[derive(Debug, Clone, PartialEq, Deserialize)]
672#[serde(rename_all = "camelCase", deny_unknown_fields)]
673pub struct ArchiveReportRequest {
674    pub archive_path: PathBuf,
675    pub run_id: String,
676    pub generated_at: String,
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub integrity: Option<Value>,
679    #[serde(default, deserialize_with = "deserialize_exit_code")]
680    pub test_exit_code: ExitCodeInput,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq, Default)]
684pub enum ExitCodeInput {
685    #[default]
686    Missing,
687    Present(Option<i32>),
688}
689
690fn deserialize_exit_code<'de, D>(deserializer: D) -> Result<ExitCodeInput, D::Error>
691where
692    D: serde::Deserializer<'de>,
693{
694    Option::<i32>::deserialize(deserializer).map(ExitCodeInput::Present)
695}
696
697#[derive(Debug, Clone, PartialEq, Eq)]
698pub enum ReportError {
699    Analysis(AnalysisError),
700    DecisionAnalysis {
701        decision_id: String,
702        error: AnalysisError,
703    },
704    InvalidEvent(String),
705    InvalidServerRecord(String),
706    InvalidArchive(String),
707    MissingManifest,
708    InvalidJson {
709        path: String,
710        reason: String,
711    },
712    ScopeMismatch {
713        expected: String,
714        actual: String,
715    },
716    NoEvidence(String),
717}
718
719impl From<AnalysisError> for ReportError {
720    fn from(value: AnalysisError) -> Self {
721        Self::Analysis(value)
722    }
723}
724
725#[derive(Clone, Default)]
726struct OrderedVectors {
727    values: Vec<McdcVector>,
728    indexes: HashMap<String, usize>,
729}
730
731impl OrderedVectors {
732    fn insert(&mut self, vector: &McdcVector) -> usize {
733        let key = vector_key(vector);
734        if let Some(index) = self.indexes.get(&key) {
735            return *index;
736        }
737        let index = self.values.len();
738        self.values.push(vector.clone());
739        self.indexes.insert(key, index);
740        index
741    }
742}
743
744#[derive(Clone)]
745struct MutableObservation {
746    vector: McdcVector,
747    tests: BTreeSet<String>,
748    phases: BTreeSet<String>,
749    explicit_phases: BTreeSet<String>,
750}
751
752#[derive(Clone)]
753struct MutableTest {
754    id: String,
755    name: String,
756    file: Option<String>,
757    title: Option<String>,
758    retries: BTreeSet<usize>,
759    attempts: BTreeMap<usize, TestAttempt>,
760    unstarted: bool,
761    runner_reported_flaky: bool,
762    provenance: TestProvenance,
763    role: String,
764    hits: BTreeSet<String>,
765    decisions: BTreeMap<String, OrderedVectors>,
766}
767
768#[derive(Clone)]
769struct MutablePhase {
770    phase: CoveragePhase,
771    test: String,
772    hits: BTreeSet<String>,
773    decisions: BTreeMap<String, OrderedVectors>,
774    browser_events: usize,
775    server_events: usize,
776    explicit_events: usize,
777    inferred_events: usize,
778    explicit_browser_events: usize,
779    inferred_browser_events: usize,
780    explicit_server_events: usize,
781    inferred_server_events: usize,
782}
783
784fn vector_key(vector: &McdcVector) -> String {
785    let mut key = String::with_capacity(vector.values.len() + 2);
786    for value in &vector.values {
787        key.push(match value {
788            None => '-',
789            Some(false) => 'F',
790            Some(true) => 'T',
791        });
792    }
793    key.push(':');
794    key.push(if vector.outcome { 'T' } else { 'F' });
795    key
796}
797
798fn sorted<T: Clone + Ord>(values: &BTreeSet<T>) -> Vec<T> {
799    values.iter().cloned().collect()
800}
801
802fn record_attempt(test: &mut MutableTest, raw: &RawTestResult) {
803    let (Some(retry), Some(raw_status)) = (raw.retry, raw.status.as_ref()) else {
804        return;
805    };
806    let previous = test.attempts.get(&retry);
807    let status = if raw_status == "unknown" {
808        previous.map_or_else(|| raw_status.clone(), |attempt| attempt.status.clone())
809    } else {
810        raw_status.clone()
811    };
812    let expected_status = raw
813        .expected_status
814        .clone()
815        .or_else(|| previous.and_then(|attempt| attempt.expected_status.clone()));
816    test.attempts.insert(
817        retry,
818        TestAttempt {
819            retry,
820            status,
821            expected_status,
822        },
823    );
824}
825
826fn effective_attempt_status(attempt: &TestAttempt) -> &str {
827    if attempt.expected_status.as_deref() == Some("failed") {
828        match attempt.status.as_str() {
829            "failed" => "passed",
830            "passed" => "failed",
831            status => status,
832        }
833    } else {
834        attempt.status.as_str()
835    }
836}
837
838fn test_outcome(test: &MutableTest) -> String {
839    let Some(terminal) = test.attempts.values().next_back() else {
840        return if test.unstarted {
841            "unstarted".into()
842        } else {
843            "unknown".into()
844        };
845    };
846    let terminal_status = effective_attempt_status(terminal);
847    if terminal_status == "passed"
848        && (test.runner_reported_flaky
849            || test
850                .attempts
851                .values()
852                .take(test.attempts.len().saturating_sub(1))
853                .any(|attempt| effective_attempt_status(attempt) != "passed"))
854    {
855        "flaky".into()
856    } else {
857        terminal_status.into()
858    }
859}
860
861fn raw_test_id(raw: &RawTestResult) -> &str {
862    raw.test_id.as_deref().unwrap_or(&raw.test)
863}
864
865pub fn passing_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
866    let mut attempts: BTreeMap<(String, usize), (BTreeSet<String>, bool)> = BTreeMap::new();
867    for raw in raw_results {
868        let Some(retry) = raw.retry else {
869            continue;
870        };
871        let entry = attempts
872            .entry((raw_test_id(raw).into(), retry))
873            .or_default();
874        if let Some(status) = &raw.status {
875            entry.0.insert(status.clone());
876        }
877        entry.1 |= raw.expected_status.as_deref() == Some("failed");
878    }
879    let mut terminal_retries = BTreeMap::<String, usize>::new();
880    for (test, retry) in attempts.keys() {
881        terminal_retries
882            .entry(test.clone())
883            .and_modify(|value| *value = (*value).max(*retry))
884            .or_insert(*retry);
885    }
886    let accepted = terminal_retries
887        .into_iter()
888        .filter_map(|(test, retry)| {
889            let (statuses, expected_failure) = attempts.get(&(test.clone(), retry))?;
890            (statuses.contains("passed") && !expected_failure).then_some((test, retry))
891        })
892        .collect::<BTreeSet<_>>();
893    raw_results
894        .iter()
895        .filter(|raw| {
896            raw.retry
897                .is_some_and(|retry| accepted.contains(&(raw_test_id(raw).into(), retry)))
898        })
899        .cloned()
900        .collect()
901}
902
903pub fn failed_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
904    let mut attempts = BTreeMap::<(String, usize), Vec<&RawTestResult>>::new();
905    for raw in raw_results {
906        if let Some(retry) = raw.retry {
907            attempts
908                .entry((raw_test_id(raw).to_owned(), retry))
909                .or_default()
910                .push(raw);
911        }
912    }
913    let failed = attempts
914        .into_iter()
915        .filter_map(|(identity, records)| {
916            // Runner reporters carry expected-status semantics while hook
917            // evidence records often do not. Treat reporter records as the
918            // authority for the attempt outcome, then retain every companion
919            // evidence record only after the attempt is classified.
920            let authoritative = records
921                .iter()
922                .copied()
923                .filter(|raw| raw.expected_status.is_some())
924                .collect::<Vec<_>>();
925            let authoritative = if authoritative.is_empty() {
926                records
927            } else {
928                authoritative
929            };
930            authoritative
931                .iter()
932                .any(
933                    |raw| match (raw.status.as_deref(), raw.expected_status.as_deref()) {
934                        (Some("failed"), Some("failed")) => false,
935                        (Some("passed"), Some("failed")) => true,
936                        (Some("failed"), _) => true,
937                        _ => false,
938                    },
939                )
940                .then_some(identity)
941        })
942        .collect::<BTreeSet<_>>();
943    raw_results
944        .iter()
945        .filter(|raw| {
946            raw.retry
947                .is_some_and(|retry| failed.contains(&(raw_test_id(raw).into(), retry)))
948        })
949        .cloned()
950        .collect()
951}
952
953pub(crate) fn javascript_coverage_model() -> CoverageModelDeclaration {
954    CoverageModelDeclaration {
955        language: "javascript".into(),
956        variant: "masking-short-circuit".into(),
957        name: "coverage-completeness-v2".into(),
958        completeness_meaning: "Every obligation in the measured model was observed by at least one existing test; test assertions and product correctness are separate assumptions.".into(),
959        measured: [
960            "executable source lines",
961            "executable statements",
962            "function entries",
963            "true and false outcomes of if, ternary, while, do/while, and classic for decisions",
964            "true and false outcomes of every atomic condition in those decisions",
965            "masking MC/DC independence for every atomic condition in those decisions",
966            "short-circuit and right-evaluated selections for &&, ||, and ?? value expressions, including JSX",
967            "short-circuit and evaluated alternatives for logical assignments and optional chains",
968            "provided and default-evaluated parameter and destructuring values",
969            "try success and catch entry",
970            "zero and entered for-in/for-of loops",
971            "entered switch cases, defaults, and implicit no-match alternatives",
972        ]
973        .into_iter()
974        .map(str::to_owned)
975        .collect(),
976        not_measured: [
977            "all input values or semantic input partitions",
978            "all execution paths or ordering/concurrency interleavings",
979            "destructuring defaults in classic for initializers (reported as blockers when discovered)",
980            "the internal statements and decisions of runtime-generated eval/Function source",
981            "mutation score or assertion fault-detection strength",
982        ]
983        .into_iter()
984        .map(str::to_owned)
985        .collect(),
986    }
987}
988
989fn summary_for_results(
990    decisions: &[DecisionResult],
991    points: &[PointResult],
992    branches: &[BranchResult],
993    lines: &[LineResult],
994    test_ids: Option<&BTreeSet<String>>,
995) -> Result<CoverageSummary, ReportError> {
996    let includes = |tests: &[String], covered: bool| {
997        test_ids.map_or(covered, |selected| {
998            tests.iter().any(|test| selected.contains(test))
999        })
1000    };
1001    let input = CoverageCoreInput {
1002        decisions: decisions
1003            .iter()
1004            .map(|decision| DecisionCoverage {
1005                condition_count: decision.meta.conditions.len(),
1006                vectors: decision
1007                    .vector_observations
1008                    .iter()
1009                    .filter(|observation| includes(&observation.tests, true))
1010                    .map(|observation| observation.vector.clone())
1011                    .collect(),
1012            })
1013            .collect(),
1014        points: points
1015            .iter()
1016            .map(|point| PointCoverage {
1017                kind: point.meta.kind.clone(),
1018                covered: includes(&point.tests, point.covered),
1019            })
1020            .collect(),
1021        branches: branches
1022            .iter()
1023            .map(|branch| BranchCoverage {
1024                kind: branch.meta.kind.clone(),
1025                alternatives: branch
1026                    .alternatives
1027                    .iter()
1028                    .map(|alternative| includes(&alternative.tests, alternative.covered))
1029                    .collect(),
1030            })
1031            .collect(),
1032        // A line every frontend declined carries no coverage question, so it
1033        // is absent from the total rather than counted as uncovered.
1034        lines: lines
1035            .iter()
1036            .filter(|line| line.measured)
1037            .map(|line| includes(&line.tests, line.covered))
1038            .collect(),
1039    };
1040    Ok(analyze_core(&input)?.summary)
1041}
1042
1043/// Recompute every structural coverage metric for an arbitrary set of test,
1044/// setup, and background evidence identities. MC/DC witnesses are rebuilt
1045/// from the selected observations; existing aggregate verdicts are never
1046/// reused.
1047pub fn coverage_summary_for_tests(
1048    view: &CoverageView,
1049    test_ids: &BTreeSet<String>,
1050) -> Result<CoverageSummary, ReportError> {
1051    summary_for_results(
1052        &view.decisions,
1053        &view.points,
1054        &view.branches,
1055        &view.lines,
1056        Some(test_ids),
1057    )
1058}
1059
1060/// Whether a manifest limitation blocks measurement of the denominator, as
1061/// opposed to declaring a boundary of it. Absent means blocking.
1062pub fn blocking_limitation(limitation: &Value) -> bool {
1063    limitation
1064        .get("blocking")
1065        .and_then(Value::as_bool)
1066        .unwrap_or(true)
1067}
1068
1069fn confidence_for(
1070    test_ids: impl IntoIterator<Item = String>,
1071    phase_ids: impl IntoIterator<Item = String>,
1072    explicit_phase_ids: impl IntoIterator<Item = String>,
1073    tests: &HashMap<String, MutableTest>,
1074    phases: &HashMap<String, MutablePhase>,
1075    asserted_phase_ids: &BTreeSet<String>,
1076) -> CoverageConfidence {
1077    let test_ids = test_ids.into_iter().collect::<BTreeSet<_>>();
1078    let phase_ids = phase_ids.into_iter().collect::<BTreeSet<_>>();
1079    let explicit_phase_ids = explicit_phase_ids.into_iter().collect::<BTreeSet<_>>();
1080    let asserted_phases = explicit_phase_ids
1081        .iter()
1082        .filter(|id| asserted_phase_ids.contains(*id))
1083        .collect::<Vec<_>>();
1084    let asserted_tests = asserted_phases
1085        .iter()
1086        .filter_map(|id| phases.get(*id).map(|phase| phase.test.clone()))
1087        .collect::<BTreeSet<_>>();
1088    let provenances = test_ids
1089        .iter()
1090        .filter_map(|id| tests.get(id).map(|test| &test.provenance))
1091        .collect::<Vec<_>>();
1092    let roles = test_ids
1093        .iter()
1094        .filter_map(|id| tests.get(id).map(|test| test.role.as_str()))
1095        .collect::<Vec<_>>();
1096    let phase_kinds = phase_ids
1097        .iter()
1098        .filter_map(|id| phases.get(id).map(|phase| phase.phase.kind.as_str()))
1099        .collect::<Vec<_>>();
1100    let only = |phase_kind: &str, role: &str| {
1101        if phase_kinds.is_empty() {
1102            !roles.is_empty() && roles.iter().all(|value| *value == role)
1103        } else {
1104            phase_kinds.iter().all(|value| *value == phase_kind)
1105        }
1106    };
1107    let has_action = explicit_phase_ids.iter().any(|id| {
1108        phases
1109            .get(id)
1110            .is_some_and(|phase| phase.phase.kind == "action")
1111    });
1112    let level = if test_ids.is_empty() {
1113        "unexecuted"
1114    } else if !asserted_tests.is_empty() {
1115        "asserted"
1116    } else if has_action {
1117        "action"
1118    } else {
1119        "executed"
1120    };
1121    let runners = provenances
1122        .iter()
1123        .map(|provenance| provenance.runner.clone())
1124        .collect::<BTreeSet<_>>();
1125    let kinds = provenances
1126        .iter()
1127        .map(|provenance| provenance.kind.clone())
1128        .collect::<BTreeSet<_>>();
1129    CoverageConfidence {
1130        level: level.into(),
1131        setup_only: only("setup", "setup"),
1132        background_only: only("background", "background"),
1133        asserted: !asserted_tests.is_empty(),
1134        tests: sorted(&test_ids),
1135        asserted_tests: sorted(&asserted_tests),
1136        runners: sorted(&runners),
1137        e2e: kinds.contains("e2e"),
1138        kinds: sorted(&kinds),
1139    }
1140}
1141
1142fn add_reference(map: &mut HashMap<String, BTreeSet<String>>, id: &str, value: &str) {
1143    map.entry(id.into()).or_default().insert(value.into());
1144}
1145
1146pub fn create_coverage_view(
1147    manifest: &CoverageManifest,
1148    raw_results: &[RawTestResult],
1149    generated_at: &str,
1150) -> Result<CoverageView, ReportError> {
1151    create_coverage_view_with_model(
1152        manifest,
1153        raw_results,
1154        generated_at,
1155        &javascript_coverage_model(),
1156    )
1157}
1158
1159fn create_coverage_view_with_model(
1160    manifest: &CoverageManifest,
1161    raw_results: &[RawTestResult],
1162    generated_at: &str,
1163    coverage_model: &CoverageModelDeclaration,
1164) -> Result<CoverageView, ReportError> {
1165    let mut decision_metadata = manifest.decisions.clone();
1166    let decision_indexes = decision_metadata
1167        .iter()
1168        .enumerate()
1169        .map(|(index, meta)| (meta.id.clone(), index))
1170        .collect::<HashMap<_, _>>();
1171    let manifest_files = manifest
1172        .decisions
1173        .iter()
1174        .map(|meta| meta.file.clone())
1175        .chain(manifest.points.iter().map(|meta| meta.file.clone()))
1176        .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
1177        .chain(
1178            manifest
1179                .scope
1180                .as_ref()
1181                .and_then(|scope| scope.get("entries"))
1182                .and_then(Value::as_array)
1183                .into_iter()
1184                .flatten()
1185                .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
1186                .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
1187        )
1188        .collect::<BTreeSet<_>>();
1189    let mut vectors_by_decision = HashMap::<String, Vec<MutableObservation>>::new();
1190    let mut vector_indexes = HashMap::<String, HashMap<String, usize>>::new();
1191    let mut tests_by_decision = HashMap::<String, BTreeSet<String>>::new();
1192    let mut tests_by_hit = HashMap::<String, BTreeSet<String>>::new();
1193    let mut tests_by_id = HashMap::<String, MutableTest>::new();
1194    let mut test_order = Vec::<String>::new();
1195    let mut phases_by_id = HashMap::<String, MutablePhase>::new();
1196    let mut phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1197    let mut explicit_phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1198
1199    for raw in raw_results {
1200        let id = raw_test_id(raw).to_owned();
1201        if !tests_by_id.contains_key(&id) {
1202            test_order.push(id.clone());
1203            tests_by_id.insert(
1204                id.clone(),
1205                MutableTest {
1206                    id: id.clone(),
1207                    name: raw.test.clone(),
1208                    file: raw.test_file.clone(),
1209                    title: raw.title.clone(),
1210                    retries: raw.retry.into_iter().collect(),
1211                    attempts: BTreeMap::new(),
1212                    unstarted: raw.status.as_deref() == Some("unstarted"),
1213                    runner_reported_flaky: raw.flaky,
1214                    provenance: raw.provenance.clone(),
1215                    role: raw.role.clone(),
1216                    hits: BTreeSet::new(),
1217                    decisions: BTreeMap::new(),
1218                },
1219            );
1220        }
1221        let test = tests_by_id.get_mut(&id).expect("test was inserted");
1222        test.unstarted |= raw.status.as_deref() == Some("unstarted");
1223        if let Some(retry) = raw.retry {
1224            test.retries.insert(retry);
1225        }
1226        test.runner_reported_flaky |= raw.flaky;
1227        record_attempt(test, raw);
1228
1229        let mut ordered_phases = raw.phases.clone();
1230        ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1231        for phase in &ordered_phases {
1232            phases_by_id.insert(
1233                phase.id.clone(),
1234                MutablePhase {
1235                    phase: phase.clone(),
1236                    test: id.clone(),
1237                    hits: BTreeSet::new(),
1238                    decisions: BTreeMap::new(),
1239                    browser_events: 0,
1240                    server_events: 0,
1241                    explicit_events: 0,
1242                    inferred_events: 0,
1243                    explicit_browser_events: 0,
1244                    inferred_browser_events: 0,
1245                    explicit_server_events: 0,
1246                    inferred_server_events: 0,
1247                },
1248            );
1249        }
1250
1251        let correlate = |event: &RuntimeEvent| {
1252            event.phase_id.clone().or_else(|| {
1253                ordered_phases
1254                    .iter()
1255                    .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1256                    .last()
1257                    .map(|phase| phase.id.clone())
1258            })
1259        };
1260
1261        let snapshots = raw.runtime.iter().chain(&raw.browser);
1262        for snapshot in snapshots {
1263            for decision in &snapshot.decisions {
1264                let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1265                    if manifest_files.contains(&decision.meta.file) {
1266                        return Err(ReportError::InvalidServerRecord(format!(
1267                            "decision {} is absent from the frozen manifest",
1268                            decision.meta.id
1269                        )));
1270                    }
1271                    continue;
1272                };
1273                if decision_metadata[index] != decision.meta {
1274                    return Err(ReportError::InvalidServerRecord(format!(
1275                        "decision {} metadata differs from the frozen manifest",
1276                        decision.meta.id
1277                    )));
1278                }
1279                for vector in &decision.vectors {
1280                    let key = vector_key(vector);
1281                    let indexes = vector_indexes.entry(decision.meta.id.clone()).or_default();
1282                    let observations = vectors_by_decision
1283                        .entry(decision.meta.id.clone())
1284                        .or_default();
1285                    let observation_index = *indexes.entry(key.clone()).or_insert_with(|| {
1286                        observations.push(MutableObservation {
1287                            vector: vector.clone(),
1288                            tests: BTreeSet::new(),
1289                            phases: BTreeSet::new(),
1290                            explicit_phases: BTreeSet::new(),
1291                        });
1292                        observations.len() - 1
1293                    });
1294                    observations[observation_index].tests.insert(id.clone());
1295                    tests_by_id
1296                        .get_mut(&id)
1297                        .expect("registered test")
1298                        .decisions
1299                        .entry(decision.meta.id.clone())
1300                        .or_default()
1301                        .insert(vector);
1302                }
1303                if !decision.vectors.is_empty() {
1304                    add_reference(&mut tests_by_decision, &decision.meta.id, &id);
1305                }
1306            }
1307            for hit in &snapshot.hits {
1308                add_reference(&mut tests_by_hit, hit, &id);
1309                tests_by_id
1310                    .get_mut(&id)
1311                    .expect("registered test")
1312                    .hits
1313                    .insert(hit.clone());
1314            }
1315            for event in &snapshot.events {
1316                let explicit = event.phase_id.is_some();
1317                let Some(phase_id) = correlate(event) else {
1318                    continue;
1319                };
1320                let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1321                    continue;
1322                };
1323                if event.environment == "browser" {
1324                    phase.browser_events += 1;
1325                    if explicit {
1326                        phase.explicit_browser_events += 1;
1327                    } else {
1328                        phase.inferred_browser_events += 1;
1329                    }
1330                } else {
1331                    phase.server_events += 1;
1332                    if explicit {
1333                        phase.explicit_server_events += 1;
1334                    } else {
1335                        phase.inferred_server_events += 1;
1336                    }
1337                }
1338                if explicit {
1339                    phase.explicit_events += 1;
1340                } else {
1341                    phase.inferred_events += 1;
1342                }
1343                if event.event_type == "hit" {
1344                    phase.hits.insert(event.id.clone());
1345                    add_reference(&mut phases_by_hit, &event.id, &phase_id);
1346                    if explicit {
1347                        add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1348                    }
1349                } else if event.event_type == "decision" {
1350                    let vector = event
1351                        .vector
1352                        .as_ref()
1353                        .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1354                    phase
1355                        .decisions
1356                        .entry(event.id.clone())
1357                        .or_default()
1358                        .insert(vector);
1359                    if let Some(index) = vector_indexes
1360                        .get(&event.id)
1361                        .and_then(|indexes| indexes.get(&vector_key(vector)))
1362                        .copied()
1363                        && let Some(observation) = vectors_by_decision
1364                            .get_mut(&event.id)
1365                            .and_then(|observations| observations.get_mut(index))
1366                    {
1367                        observation.phases.insert(phase_id.clone());
1368                        if explicit {
1369                            observation.explicit_phases.insert(phase_id.clone());
1370                        }
1371                    }
1372                } else {
1373                    return Err(ReportError::InvalidEvent(event.event_type.clone()));
1374                }
1375            }
1376        }
1377
1378        for record in &raw.server {
1379            let (record_id, decision) = if record.record_type == "decision" {
1380                let meta = record
1381                    .meta
1382                    .as_ref()
1383                    .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1384                let vector = record
1385                    .vector
1386                    .as_ref()
1387                    .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1388                let Some(index) = decision_indexes.get(&meta.id).copied() else {
1389                    if manifest_files.contains(&meta.file) {
1390                        return Err(ReportError::InvalidServerRecord(format!(
1391                            "decision {} is absent from the frozen manifest",
1392                            meta.id
1393                        )));
1394                    }
1395                    continue;
1396                };
1397                if decision_metadata[index] != *meta {
1398                    return Err(ReportError::InvalidServerRecord(format!(
1399                        "decision {} metadata differs from the frozen manifest",
1400                        meta.id
1401                    )));
1402                }
1403                let key = vector_key(vector);
1404                let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1405                let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1406                let index = *indexes.entry(key).or_insert_with(|| {
1407                    observations.push(MutableObservation {
1408                        vector: vector.clone(),
1409                        tests: BTreeSet::new(),
1410                        phases: BTreeSet::new(),
1411                        explicit_phases: BTreeSet::new(),
1412                    });
1413                    observations.len() - 1
1414                });
1415                observations[index].tests.insert(id.clone());
1416                tests_by_id
1417                    .get_mut(&id)
1418                    .expect("registered test")
1419                    .decisions
1420                    .entry(meta.id.clone())
1421                    .or_default()
1422                    .insert(vector);
1423                add_reference(&mut tests_by_decision, &meta.id, &id);
1424                (meta.id.clone(), Some(vector.clone()))
1425            } else if record.record_type == "hit" {
1426                let hit = record
1427                    .id
1428                    .as_ref()
1429                    .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1430                add_reference(&mut tests_by_hit, hit, &id);
1431                tests_by_id
1432                    .get_mut(&id)
1433                    .expect("registered test")
1434                    .hits
1435                    .insert(hit.clone());
1436                (hit.clone(), None)
1437            } else {
1438                return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1439            };
1440            let Some(timestamp_ms) = record.timestamp_ms else {
1441                continue;
1442            };
1443            let event = RuntimeEvent {
1444                event_type: record.record_type.clone(),
1445                id: record_id,
1446                vector: decision,
1447                timestamp_ms,
1448                phase_id: record.phase_id.clone(),
1449                environment: "server".into(),
1450            };
1451            let explicit = event.phase_id.is_some();
1452            let phase_id = correlate(&event);
1453            let Some(phase_id) = phase_id else { continue };
1454            let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1455                continue;
1456            };
1457            phase.server_events += 1;
1458            if explicit {
1459                phase.explicit_events += 1;
1460                phase.explicit_server_events += 1;
1461            } else {
1462                phase.inferred_events += 1;
1463                phase.inferred_server_events += 1;
1464            }
1465            if event.event_type == "hit" {
1466                phase.hits.insert(event.id.clone());
1467                add_reference(&mut phases_by_hit, &event.id, &phase_id);
1468                if explicit {
1469                    add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1470                }
1471            } else if let Some(vector) = &event.vector {
1472                phase
1473                    .decisions
1474                    .entry(event.id.clone())
1475                    .or_default()
1476                    .insert(vector);
1477                if let Some(index) = vector_indexes
1478                    .get(&event.id)
1479                    .and_then(|indexes| indexes.get(&vector_key(vector)))
1480                    .copied()
1481                    && let Some(observation) = vectors_by_decision
1482                        .get_mut(&event.id)
1483                        .and_then(|observations| observations.get_mut(index))
1484                {
1485                    observation.phases.insert(phase_id.clone());
1486                    if explicit {
1487                        observation.explicit_phases.insert(phase_id.clone());
1488                    }
1489                }
1490            }
1491        }
1492    }
1493
1494    let mut asserted_phase_ids = BTreeSet::new();
1495    for phase in phases_by_id.values() {
1496        if phase.phase.kind == "assertion" && phase.phase.status.as_deref() == Some("passed") {
1497            asserted_phase_ids.insert(phase.phase.id.clone());
1498            if let Some(cause) = &phase.phase.caused_by_phase_id {
1499                asserted_phase_ids.insert(cause.clone());
1500            }
1501        }
1502    }
1503
1504    decision_metadata.sort_by(|left, right| {
1505        left.file
1506            .cmp(&right.file)
1507            .then(left.line.cmp(&right.line))
1508            .then(left.column.cmp(&right.column))
1509    });
1510    let mut decisions = Vec::with_capacity(decision_metadata.len());
1511    for meta in decision_metadata {
1512        let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
1513        let mut observations = Vec::with_capacity(mutable.len());
1514        for observation in mutable {
1515            let confidence = confidence_for(
1516                sorted(&observation.tests),
1517                sorted(&observation.phases),
1518                sorted(&observation.explicit_phases),
1519                &tests_by_id,
1520                &phases_by_id,
1521                &asserted_phase_ids,
1522            );
1523            observations.push(VectorObservation {
1524                vector: observation.vector,
1525                tests: sorted(&observation.tests),
1526                phases: sorted(&observation.phases),
1527                explicit_phases: sorted(&observation.explicit_phases),
1528                confidence,
1529            });
1530        }
1531        let vectors = observations
1532            .iter()
1533            .map(|observation| observation.vector.clone())
1534            .collect::<Vec<_>>();
1535        let witnesses =
1536            find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
1537                ReportError::DecisionAnalysis {
1538                    decision_id: meta.id.clone(),
1539                    error,
1540                }
1541            })?;
1542        let mut conditions = Vec::with_capacity(meta.conditions.len());
1543        for (index, source) in meta.conditions.iter().enumerate() {
1544            let witness = witnesses[index].map(|witness| {
1545                [
1546                    vectors[witness.first].clone(),
1547                    vectors[witness.second].clone(),
1548                ]
1549            });
1550            let witness_tests = witnesses[index].map(|witness| {
1551                [
1552                    observations[witness.first].tests.clone(),
1553                    observations[witness.second].tests.clone(),
1554                ]
1555            });
1556            let assertion_covered = (0..observations.len()).any(|left| {
1557                ((left + 1)..observations.len()).any(|right| {
1558                    observations[left].confidence.asserted
1559                        && observations[right].confidence.asserted
1560                        && crate::coverage_analysis::is_independence_pair(
1561                            &observations[left].vector,
1562                            &observations[right].vector,
1563                            index,
1564                        )
1565                })
1566            });
1567            conditions.push(ConditionResult {
1568                index,
1569                source: source.clone(),
1570                covered: witness.is_some(),
1571                assertion_covered,
1572                witness,
1573                witness_tests,
1574            });
1575        }
1576        let decision_tests = tests_by_decision.remove(&meta.id).unwrap_or_default();
1577        let confidence = confidence_for(
1578            sorted(&decision_tests),
1579            observations
1580                .iter()
1581                .flat_map(|observation| observation.phases.clone()),
1582            observations
1583                .iter()
1584                .flat_map(|observation| observation.explicit_phases.clone()),
1585            &tests_by_id,
1586            &phases_by_id,
1587            &asserted_phase_ids,
1588        );
1589        decisions.push(DecisionResult {
1590            executed: !vectors.is_empty(),
1591            covered: conditions.iter().all(|condition| condition.covered),
1592            meta,
1593            vectors,
1594            vector_observations: observations,
1595            conditions,
1596            tests: sorted(&decision_tests),
1597            confidence,
1598        });
1599    }
1600
1601    let points = manifest
1602        .points
1603        .iter()
1604        .cloned()
1605        .map(|meta| {
1606            let tests = tests_by_hit.get(&meta.id).cloned().unwrap_or_default();
1607            let phases = phases_by_hit.get(&meta.id).cloned().unwrap_or_default();
1608            let explicit = explicit_phases_by_hit
1609                .get(&meta.id)
1610                .cloned()
1611                .unwrap_or_default();
1612            PointResult {
1613                covered: tests_by_hit.contains_key(&meta.id),
1614                confidence: confidence_for(
1615                    sorted(&tests),
1616                    sorted(&phases),
1617                    sorted(&explicit),
1618                    &tests_by_id,
1619                    &phases_by_id,
1620                    &asserted_phase_ids,
1621                ),
1622                meta,
1623                tests: sorted(&tests),
1624                phases: sorted(&phases),
1625            }
1626        })
1627        .collect::<Vec<_>>();
1628
1629    let branches = manifest
1630        .branches
1631        .iter()
1632        .cloned()
1633        .map(|meta| {
1634            let alternatives = meta
1635                .alternatives
1636                .iter()
1637                .map(|alternative| {
1638                    let tests = tests_by_hit
1639                        .get(&alternative.id)
1640                        .cloned()
1641                        .unwrap_or_default();
1642                    let phases = phases_by_hit
1643                        .get(&alternative.id)
1644                        .cloned()
1645                        .unwrap_or_default();
1646                    let explicit = explicit_phases_by_hit
1647                        .get(&alternative.id)
1648                        .cloned()
1649                        .unwrap_or_default();
1650                    AlternativeResult {
1651                        id: alternative.id.clone(),
1652                        label: alternative.label.clone(),
1653                        covered: tests_by_hit.contains_key(&alternative.id),
1654                        tests: sorted(&tests),
1655                        phases: sorted(&phases),
1656                        confidence: confidence_for(
1657                            sorted(&tests),
1658                            sorted(&phases),
1659                            sorted(&explicit),
1660                            &tests_by_id,
1661                            &phases_by_id,
1662                            &asserted_phase_ids,
1663                        ),
1664                    }
1665                })
1666                .collect::<Vec<_>>();
1667            BranchResult {
1668                covered: alternatives.iter().all(|alternative| alternative.covered),
1669                meta,
1670                alternatives,
1671            }
1672        })
1673        .collect::<Vec<_>>();
1674
1675    // Obligations the frontend declined to measure leave the covered/uncovered
1676    // denominator entirely. Counting them as uncovered would report a
1677    // measurement gap as a coverage gap — a wrong number, and wrong numbers get
1678    // trusted. They are reported separately instead, alongside the share of
1679    // obligations that were measured exactly.
1680    let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
1681
1682    // Lines are folded from every point, declined ones included, so a file
1683    // Supercov could not measure still has addressable lines to report a
1684    // limitation against. Only what was measured decides the line's state and
1685    // whether it counts.
1686    let mut line_aggregates = BTreeMap::<SourceLine, LineAggregate>::new();
1687    for point in &points {
1688        let aggregate = line_aggregates
1689            .entry(SourceLine {
1690                file: point.meta.file.clone(),
1691                line: point.meta.line,
1692            })
1693            .or_default();
1694        if declined.contains(&point.meta.id) {
1695            continue;
1696        }
1697        aggregate.measured = true;
1698        aggregate.covered |= point.covered;
1699        aggregate.tests.extend(point.tests.clone());
1700        aggregate.phases.extend(point.phases.clone());
1701        aggregate.explicit_phases.extend(
1702            explicit_phases_by_hit
1703                .get(&point.meta.id)
1704                .into_iter()
1705                .flatten()
1706                .cloned(),
1707        );
1708    }
1709    let lines = line_aggregates
1710        .into_iter()
1711        .map(|(location, aggregate)| {
1712            let LineAggregate {
1713                covered,
1714                measured,
1715                tests: test_ids,
1716                phases: phase_ids,
1717                explicit_phases: explicit_ids,
1718            } = aggregate;
1719            let provenances = test_ids
1720                .iter()
1721                .filter_map(|id| tests_by_id.get(id).map(|test| &test.provenance))
1722                .collect::<Vec<_>>();
1723            let runners = provenances
1724                .iter()
1725                .map(|provenance| provenance.runner.clone())
1726                .collect::<BTreeSet<_>>();
1727            let kinds = provenances
1728                .iter()
1729                .map(|provenance| provenance.kind.clone())
1730                .collect::<BTreeSet<_>>();
1731            LineResult {
1732                file: location.file,
1733                line: location.line,
1734                covered,
1735                measured,
1736                tests: sorted(&test_ids),
1737                runners: sorted(&runners),
1738                exclusive_kind: (kinds.len() == 1).then(|| kinds.first().unwrap().clone()),
1739                phases: sorted(&phase_ids),
1740                confidence: confidence_for(
1741                    sorted(&test_ids),
1742                    sorted(&phase_ids),
1743                    sorted(&explicit_ids),
1744                    &tests_by_id,
1745                    &phases_by_id,
1746                    &asserted_phase_ids,
1747                ),
1748                kinds: sorted(&kinds),
1749            }
1750        })
1751        .collect::<Vec<_>>();
1752
1753    let point_locations = manifest
1754        .points
1755        .iter()
1756        .map(|point| {
1757            (
1758                point.id.clone(),
1759                SourceLine {
1760                    file: point.file.clone(),
1761                    line: point.line,
1762                },
1763            )
1764        })
1765        .collect::<HashMap<_, _>>();
1766    test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
1767    let tests = test_order
1768        .into_iter()
1769        .map(|id| {
1770            let test = tests_by_id.get(&id).expect("test order references test");
1771            let lines = test
1772                .hits
1773                .iter()
1774                .filter_map(|hit| point_locations.get(hit).cloned())
1775                .collect::<BTreeSet<_>>();
1776            TestCoverageResult {
1777                id: test.id.clone(),
1778                name: test.name.clone(),
1779                file: test.file.clone(),
1780                title: test.title.clone(),
1781                retries: sorted(&test.retries),
1782                attempts: test.attempts.values().cloned().collect(),
1783                outcome: test_outcome(test),
1784                provenance: test.provenance.clone(),
1785                role: test.role.clone(),
1786                hits: sorted(&test.hits),
1787                decisions: test
1788                    .decisions
1789                    .iter()
1790                    .map(|(id, vectors)| TestDecisionResult {
1791                        id: id.clone(),
1792                        vectors: vectors.values.clone(),
1793                    })
1794                    .collect(),
1795                lines: lines.into_iter().collect(),
1796            }
1797        })
1798        .collect::<Vec<_>>();
1799
1800    let mut test_files = BTreeMap::<
1801        String,
1802        (
1803            BTreeSet<String>,
1804            BTreeSet<String>,
1805            BTreeSet<String>,
1806            BTreeSet<SourceLine>,
1807        ),
1808    >::new();
1809    for test in &tests {
1810        let aggregate = test_files
1811            .entry(
1812                test.file
1813                    .clone()
1814                    .unwrap_or_else(|| "(unknown test file)".into()),
1815            )
1816            .or_default();
1817        aggregate.0.insert(test.id.clone());
1818        aggregate.1.insert(test.provenance.runner.clone());
1819        aggregate.2.insert(test.provenance.kind.clone());
1820        aggregate.3.extend(test.lines.clone());
1821    }
1822    let test_files = test_files
1823        .into_iter()
1824        .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
1825            file,
1826            tests: sorted(&tests),
1827            runners: sorted(&runners),
1828            kinds: sorted(&kinds),
1829            lines: lines.into_iter().collect(),
1830        })
1831        .collect::<Vec<_>>();
1832
1833    let mut phases = phases_by_id.values().cloned().collect::<Vec<_>>();
1834    phases.sort_by(|left, right| {
1835        left.phase
1836            .started_at_ms
1837            .cmp(&right.phase.started_at_ms)
1838            .then(left.phase.id.cmp(&right.phase.id))
1839    });
1840    let phases = phases
1841        .into_iter()
1842        .map(|phase| {
1843            let lines = phase
1844                .hits
1845                .iter()
1846                .filter_map(|hit| point_locations.get(hit).cloned())
1847                .collect::<BTreeSet<_>>();
1848            PhaseResult {
1849                phase: phase.phase,
1850                test: phase.test,
1851                hits: sorted(&phase.hits),
1852                decisions: phase
1853                    .decisions
1854                    .into_iter()
1855                    .map(|(id, vectors)| TestDecisionResult {
1856                        id,
1857                        vectors: vectors.values,
1858                    })
1859                    .collect(),
1860                lines: lines.into_iter().collect(),
1861                browser_events: phase.browser_events,
1862                server_events: phase.server_events,
1863                explicit_events: phase.explicit_events,
1864                inferred_events: phase.inferred_events,
1865                explicit_browser_events: phase.explicit_browser_events,
1866                inferred_browser_events: phase.inferred_browser_events,
1867                explicit_server_events: phase.explicit_server_events,
1868                inferred_server_events: phase.inferred_server_events,
1869            }
1870        })
1871        .collect::<Vec<_>>();
1872
1873    let total_obligations = decisions.len() + points.len() + branches.len();
1874    let (decisions, points, branches) = if declined.is_empty() {
1875        (decisions, points, branches)
1876    } else {
1877        (
1878            decisions
1879                .into_iter()
1880                .filter(|result| !declined.contains(&result.meta.id))
1881                .collect::<Vec<_>>(),
1882            points
1883                .into_iter()
1884                .filter(|result| !declined.contains(&result.meta.id))
1885                .collect::<Vec<_>>(),
1886            branches
1887                .into_iter()
1888                .filter(|result| !declined.contains(&result.meta.id))
1889                .collect::<Vec<_>>(),
1890        )
1891    };
1892    let measured_obligations = decisions.len() + points.len() + branches.len();
1893    let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
1894    if total_obligations > measured_obligations {
1895        summary.unmeasured_obligations = Some(total_obligations - measured_obligations);
1896        summary.exact_fraction_pct = Some(if total_obligations == 0 {
1897            100.0
1898        } else {
1899            (measured_obligations as f64) * 100.0 / (total_obligations as f64)
1900        });
1901    }
1902    // A limitation that says `"blocking": false` is a declared boundary of
1903    // the denominator -- a macro the compiler expands, a const context no
1904    // probe can run in -- not a failure to measure what is inside it. Only a
1905    // blocking one makes the run incomplete. A limitation that says nothing
1906    // is blocking, so a frontend that has not been taught the difference
1907    // keeps its behaviour.
1908    if manifest.limitations.iter().any(blocking_limitation) {
1909        summary.coverage_complete = false;
1910        summary.completeness_blocked = Some(true);
1911    }
1912
1913    let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
1914        let values = tests
1915            .iter()
1916            .map(|test| {
1917                if field == "kind" {
1918                    test.provenance.kind.clone()
1919                } else {
1920                    test.provenance.runner.clone()
1921                }
1922            })
1923            .collect::<BTreeSet<_>>();
1924        values
1925            .into_iter()
1926            .map(|value| {
1927                let selected = tests
1928                    .iter()
1929                    .filter(|test| {
1930                        if field == "kind" {
1931                            test.provenance.kind == value
1932                        } else {
1933                            test.provenance.runner == value
1934                        }
1935                    })
1936                    .map(|test| test.id.clone())
1937                    .collect::<BTreeSet<_>>();
1938                Ok(DimensionCoverage {
1939                    kind: (field == "kind").then(|| value.clone()),
1940                    runner: (field == "runner").then(|| value.clone()),
1941                    tests: tests
1942                        .iter()
1943                        .filter(|test| selected.contains(&test.id) && test.role == "test")
1944                        .count(),
1945                    setups: tests
1946                        .iter()
1947                        .filter(|test| selected.contains(&test.id) && test.role == "setup")
1948                        .count(),
1949                    summary: summary_for_results(
1950                        &decisions,
1951                        &points,
1952                        &branches,
1953                        &lines,
1954                        Some(&selected),
1955                    )?,
1956                })
1957            })
1958            .collect()
1959    };
1960
1961    Ok(CoverageView {
1962        generated_at: generated_at.into(),
1963        variant: coverage_model.variant.clone(),
1964        scope: manifest.scope.clone(),
1965        model: CoverageModel {
1966            language: coverage_model.language.clone(),
1967            name: coverage_model.name.clone(),
1968            completeness_meaning: coverage_model.completeness_meaning.clone(),
1969            measured: coverage_model.measured.clone(),
1970            not_measured: coverage_model.not_measured.clone(),
1971        },
1972        integrity: None,
1973        limitations: manifest.limitations.clone(),
1974        transport: None,
1975        summary,
1976        coverage_by_kind: dimension_coverage("kind")?,
1977        coverage_by_runner: dimension_coverage("runner")?,
1978        decisions,
1979        points,
1980        branches,
1981        tests,
1982        test_files,
1983        phases,
1984        lines,
1985    })
1986}
1987
1988pub fn analyze_coverage_results(
1989    request: &CoverageReportRequest,
1990) -> Result<CoverageReport, ReportError> {
1991    if let Some(scope) = request
1992        .raw_results
1993        .iter()
1994        .filter_map(|raw| raw.scope.as_ref())
1995        .find(|scope| scope.run_id != request.run_id)
1996    {
1997        return Err(ReportError::ScopeMismatch {
1998            expected: request.run_id.clone(),
1999            actual: scope.run_id.clone(),
2000        });
2001    }
2002    if request.raw_results.is_empty() {
2003        return Err(ReportError::NoEvidence(request.run_id.clone()));
2004    }
2005    let default_model = javascript_coverage_model();
2006    let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
2007    PersistedCoverageModel::from_declaration(coverage_model).map_err(|reason| {
2008        ReportError::InvalidJson {
2009            path: "coverage-model.json".into(),
2010            reason: reason.into(),
2011        }
2012    })?;
2013    let view = create_coverage_view_with_model(
2014        &request.manifest,
2015        &request.raw_results,
2016        &request.generated_at,
2017        coverage_model,
2018    )?;
2019    let passed = create_coverage_view_with_model(
2020        &request.manifest,
2021        &passing_coverage_results(&request.raw_results),
2022        &request.generated_at,
2023        coverage_model,
2024    )?;
2025    let failed = create_coverage_view_with_model(
2026        &request.manifest,
2027        &failed_coverage_results(&request.raw_results),
2028        &request.generated_at,
2029        coverage_model,
2030    )?;
2031    let execution = match request.test_exit_code {
2032        ExitCodeInput::Missing => None,
2033        ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
2034            valid: test_exit_code == Some(0),
2035            test_exit_code,
2036        }),
2037    };
2038    let mut view = view;
2039    let mut passed = passed;
2040    let mut failed = failed;
2041    if let Some(integrity) = &request.integrity {
2042        view.integrity = Some(integrity.clone());
2043        passed.integrity = Some(integrity.clone());
2044        failed.integrity = Some(integrity.clone());
2045    }
2046    Ok(CoverageReport {
2047        view,
2048        execution,
2049        filters: CoverageFilters { passed, failed },
2050    })
2051}
2052
2053fn parse_entry<T: for<'de> Deserialize<'de>>(
2054    entry: &EvidenceArchiveEntry,
2055) -> Result<T, ReportError> {
2056    serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
2057        path: entry.path.clone(),
2058        reason: error.to_string(),
2059    })
2060}
2061
2062fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
2063    entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2064) -> Result<Vec<T>, ReportError> {
2065    let mut records = Vec::new();
2066    for entry in entries {
2067        let Some(contents) = entry.contents.strip_suffix(b"\n") else {
2068            return Err(ReportError::InvalidJson {
2069                path: entry.path.clone(),
2070                reason: "recognized JSONL evidence must end with a newline".into(),
2071            });
2072        };
2073        if contents.is_empty() {
2074            return Err(ReportError::InvalidJson {
2075                path: entry.path.clone(),
2076                reason: "recognized JSONL evidence must contain at least one record".into(),
2077            });
2078        }
2079        for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
2080            if line.is_empty() {
2081                return Err(ReportError::InvalidJson {
2082                    path: entry.path.clone(),
2083                    reason: format!("blank JSONL record at line {}", index + 1),
2084                });
2085            }
2086            records.push(serde_json::from_slice(line).map_err(|error| {
2087                ReportError::InvalidJson {
2088                    path: entry.path.clone(),
2089                    reason: format!("invalid JSONL record at line {}: {error}", index + 1),
2090                }
2091            })?);
2092        }
2093    }
2094    Ok(records)
2095}
2096
2097/// Server evidence is appended by application processes Supercov does not
2098/// control — including pool VMs restored from one snapshot, whose clones can
2099/// tear a shared shard. A torn line is one lost record, not a lost run: it is
2100/// skipped and counted, and the report says so through the existing
2101/// CORRUPT_EVIDENCE_RECORDS diagnostic and the blocking-limitation total.
2102struct TolerantJsonLines<T> {
2103    records: Vec<T>,
2104    corrupt_records: usize,
2105    corrupt_files: usize,
2106}
2107
2108fn parse_server_json_lines<'a, T: for<'de> Deserialize<'de>>(
2109    entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2110) -> TolerantJsonLines<T> {
2111    let mut parsed = TolerantJsonLines {
2112        records: Vec::new(),
2113        corrupt_records: 0,
2114        corrupt_files: 0,
2115    };
2116    for entry in entries {
2117        let mut corrupt_here = 0;
2118        for line in entry.contents.split(|byte| *byte == b'\n') {
2119            if line.is_empty() {
2120                continue;
2121            }
2122            match serde_json::from_slice(line) {
2123                Ok(record) => parsed.records.push(record),
2124                Err(_) => corrupt_here += 1,
2125            }
2126        }
2127        if corrupt_here > 0 {
2128            parsed.corrupt_records += corrupt_here;
2129            parsed.corrupt_files += 1;
2130        }
2131    }
2132    parsed
2133}
2134
2135fn is_mcdc_result(path: &str) -> bool {
2136    path == "mcdc.json" || path.ends_with("/mcdc.json")
2137}
2138
2139fn is_mcdc_journal(path: &str) -> bool {
2140    path == "mcdc.jsonl" || path.ends_with(".mcdc.jsonl")
2141}
2142
2143fn validate_rust_compiler_scope(manifest: &CoverageManifest) -> Result<(), ReportError> {
2144    let scope = manifest
2145        .scope
2146        .as_ref()
2147        .and_then(Value::as_object)
2148        .ok_or_else(|| ReportError::InvalidArchive("missing Rust compiler source scope".into()))?;
2149    let expected = BTreeSet::from([
2150        "crate",
2151        "language",
2152        "measurementComplete",
2153        "model",
2154        "sourceFingerprint",
2155    ]);
2156    if scope.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected
2157        || scope.get("language").and_then(Value::as_str) != Some("rust")
2158        || scope.get("model").and_then(Value::as_str) != Some("rust-source-v1")
2159        || scope
2160            .get("crate")
2161            .and_then(Value::as_str)
2162            .is_none_or(str::is_empty)
2163        || !scope
2164            .get("measurementComplete")
2165            .is_some_and(Value::is_boolean)
2166    {
2167        return Err(ReportError::InvalidArchive(
2168            "malformed Rust compiler source scope".into(),
2169        ));
2170    }
2171    let fingerprint = scope
2172        .get("sourceFingerprint")
2173        .and_then(Value::as_object)
2174        .ok_or_else(|| {
2175            ReportError::InvalidArchive("missing Rust compiler source fingerprint".into())
2176        })?;
2177    let expected_fingerprint = BTreeSet::from(["algorithm", "digest", "files", "generatedFiles"]);
2178    let digest = fingerprint.get("digest").and_then(Value::as_str);
2179    let files = fingerprint.get("files").and_then(Value::as_u64);
2180    let generated = fingerprint.get("generatedFiles").and_then(Value::as_u64);
2181    if fingerprint
2182        .keys()
2183        .map(String::as_str)
2184        .collect::<BTreeSet<_>>()
2185        != expected_fingerprint
2186        || fingerprint.get("algorithm").and_then(Value::as_str) != Some("sha256")
2187        || !digest.is_some_and(|digest| {
2188            digest.len() == 64
2189                && digest
2190                    .bytes()
2191                    .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2192        })
2193        || files.is_none_or(|files| files == 0)
2194        || generated
2195            .zip(files)
2196            .is_none_or(|(generated, files)| generated > files)
2197    {
2198        return Err(ReportError::InvalidArchive(
2199            "malformed Rust compiler source fingerprint".into(),
2200        ));
2201    }
2202    Ok(())
2203}
2204
2205pub fn analyze_coverage_archive(
2206    request: &ArchiveReportRequest,
2207) -> Result<CoverageReport, ReportError> {
2208    let entries = read_archive(Path::new(&request.archive_path))
2209        .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2210    let manifest = entries
2211        .iter()
2212        .find(|entry| entry.path == "manifest.json")
2213        .ok_or(ReportError::MissingManifest)
2214        .and_then(parse_entry::<CoverageManifest>)?;
2215    let mut raw_results = entries
2216        .iter()
2217        .filter(|entry| is_mcdc_result(&entry.path))
2218        .map(parse_entry::<RawTestResult>)
2219        .collect::<Result<Vec<_>, _>>()?;
2220    let journal_results = parse_json_lines::<RawTestResult>(
2221        entries.iter().filter(|entry| is_mcdc_journal(&entry.path)),
2222    )?;
2223    raw_results.extend(journal_results);
2224
2225    let scoped = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2226        entry.path.starts_with("server/")
2227            && !entry.path.starts_with("server/background/")
2228            && entry.path.ends_with(".jsonl")
2229    }));
2230    let scoped_records = scoped.records;
2231    for record in &scoped_records {
2232        let Some(scope) = &record.scope else { continue };
2233        let Some(raw) = raw_results
2234            .iter_mut()
2235            .find(|raw| raw.scope.as_ref() == Some(scope))
2236        else {
2237            continue;
2238        };
2239        if !raw.server.contains(record) {
2240            raw.server.push(record.clone());
2241        }
2242    }
2243
2244    let background = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2245        entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2246    }));
2247    let background_records = background.records;
2248    if !background_records.is_empty() {
2249        raw_results.push(RawTestResult {
2250            test_id: Some(format!("background:{}", request.run_id)),
2251            scope: None,
2252            test: "Background / unattributed".into(),
2253            test_file: None,
2254            title: Some("Background / unattributed".into()),
2255            retry: None,
2256            status: Some("unknown".into()),
2257            expected_status: None,
2258            flaky: false,
2259            provenance: TestProvenance {
2260                runner: "background".into(),
2261                kind: "background".into(),
2262                project: None,
2263                source: "explicit".into(),
2264            },
2265            role: "background".into(),
2266            phases: vec![],
2267            runtime: vec![],
2268            browser: vec![],
2269            server: background_records.clone(),
2270        });
2271    }
2272
2273    // Execution traces are appended by the launch observer inside application
2274    // processes, so they share the clone hazard of server evidence.
2275    let execution =
2276        parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2277            entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2278        }));
2279    let execution_events = execution.records;
2280    let count_event = |name: &str| {
2281        execution_events
2282            .iter()
2283            .filter(|event| event.kind() == name)
2284            .count()
2285    };
2286    let transport = TransportStats {
2287        processes: count_event("process"),
2288        child_launches: count_event("child-launch"),
2289        remote_launches: count_event("remote-launch"),
2290        workspace_capabilities: count_event("workspace-capability"),
2291        scoped_server_records: scoped_records.len(),
2292        background_server_records: background_records.len(),
2293        corrupt_records: scoped.corrupt_records
2294            + background.corrupt_records
2295            + execution.corrupt_records,
2296        corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
2297    };
2298    let frontend = entries
2299        .iter()
2300        .find(|entry| entry.path == "frontend.json")
2301        .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
2302        .and_then(parse_entry::<FrontendRunDeclaration>)?;
2303    let persisted = entries
2304        .iter()
2305        .find(|entry| entry.path == "coverage-model.json")
2306        .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
2307        .and_then(parse_entry::<PersistedCoverageModel>)?;
2308    let coverage_model =
2309        persisted
2310            .into_declaration()
2311            .map_err(|reason| ReportError::InvalidJson {
2312                path: "coverage-model.json".into(),
2313                reason: reason.into(),
2314            })?;
2315    if frontend.language != coverage_model.language {
2316        return Err(ReportError::InvalidArchive(format!(
2317            "frontend language {} differs from coverage model language {}",
2318            frontend.language, coverage_model.language
2319        )));
2320    }
2321    if frontend.frontend_version == "rust-compiler-v1" {
2322        validate_rust_compiler_scope(&manifest)?;
2323    }
2324    let normalized = CoverageReportRequest {
2325        run_id: request.run_id.clone(),
2326        manifest,
2327        raw_results,
2328        generated_at: request.generated_at.clone(),
2329        coverage_model: Some(coverage_model),
2330        integrity: request.integrity.clone(),
2331        test_exit_code: request.test_exit_code.clone(),
2332    };
2333    let mut report = crate::frontend_protocol::analyze_frontend_results(&frontend, &normalized)
2334        .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2335    report.view.transport = Some(transport.clone());
2336    report.filters.passed.transport = Some(transport.clone());
2337    report.filters.failed.transport = Some(transport);
2338    Ok(report)
2339}
2340
2341#[cfg(test)]
2342mod tests {
2343    use std::{
2344        fs,
2345        sync::atomic::{AtomicU64, Ordering},
2346        time::SystemTime,
2347    };
2348
2349    use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
2350
2351    use super::*;
2352
2353    static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
2354
2355    fn point(id: &str, line: usize) -> PointMeta {
2356        PointMeta {
2357            id: id.into(),
2358            kind: PointKind::Statement,
2359            file: "src/app.js".into(),
2360            line,
2361            column: 0,
2362            source: "work();".into(),
2363            label: None,
2364        }
2365    }
2366
2367    fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
2368        RawTestResult {
2369            test_id: Some(id.into()),
2370            scope: None,
2371            test: id.into(),
2372            test_file: Some("tests/app.test.js".into()),
2373            title: None,
2374            retry: Some(retry),
2375            status: Some(status.into()),
2376            expected_status: None,
2377            flaky: false,
2378            provenance: TestProvenance {
2379                runner: "node:test".into(),
2380                kind: "unit".into(),
2381                project: None,
2382                source: "runner-default".into(),
2383            },
2384            role: "test".into(),
2385            phases: vec![],
2386            runtime: vec![RuntimeSnapshot {
2387                decisions: vec![],
2388                hits: hits.iter().map(|hit| (*hit).into()).collect(),
2389                events: vec![],
2390            }],
2391            browser: vec![],
2392            server: vec![],
2393        }
2394    }
2395
2396    #[test]
2397    fn declined_obligations_are_unmeasured_never_uncovered() {
2398        // Two statements, neither executed. One of them Supercov declined to
2399        // measure. The declined one must not appear as a coverage gap: it
2400        // leaves the denominator and is reported as unmeasured instead.
2401        let point = |id: &str, line: usize| PointMeta {
2402            id: id.into(),
2403            kind: PointKind::Statement,
2404            file: "src/app.js".into(),
2405            line,
2406            column: 0,
2407            source: "work();".into(),
2408            label: None,
2409        };
2410        let measured_only = CoverageManifest {
2411            decisions: vec![],
2412            points: vec![point("measured", 1)],
2413            branches: vec![],
2414            limitations: vec![],
2415            unmeasured: Vec::new(),
2416            scope: None,
2417        };
2418        let with_declined = CoverageManifest {
2419            decisions: vec![],
2420            points: vec![point("measured", 1), point("declined", 2)],
2421            branches: vec![],
2422            limitations: vec![],
2423            unmeasured: vec!["declined".into()],
2424            scope: None,
2425        };
2426
2427        let baseline =
2428            create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
2429        let view =
2430            create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
2431
2432        // The declined statement never inflates the uncovered count.
2433        assert_eq!(
2434            view.summary.statements.total, baseline.summary.statements.total,
2435            "a declined obligation stayed in the covered/uncovered denominator"
2436        );
2437        assert_eq!(view.summary.unmeasured_obligations, Some(1));
2438        assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
2439
2440        // Its line leaves the line total too, but not the report: the line is
2441        // still there to hang a limitation on, and it is neither covered nor
2442        // uncovered.
2443        assert_eq!(
2444            view.summary.lines.total, baseline.summary.lines.total,
2445            "a declined obligation kept its line in the line denominator"
2446        );
2447        let declined_line = view
2448            .lines
2449            .iter()
2450            .find(|line| line.line == 2)
2451            .expect("the declined line stays addressable");
2452        assert!(!declined_line.measured);
2453        assert!(!declined_line.covered);
2454
2455        // A fully measured run keeps its previous output exactly: the new
2456        // fields are absent, not zero, so existing consumers see no change.
2457        assert_eq!(baseline.summary.unmeasured_obligations, None);
2458        assert_eq!(baseline.summary.exact_fraction_pct, None);
2459        let encoded = serde_json::to_string(&baseline.summary).unwrap();
2460        assert!(
2461            !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
2462            "a fully measured summary must serialize unchanged: {encoded}"
2463        );
2464    }
2465
2466    #[test]
2467    fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
2468        // The subtle case: the statement ran, but Supercov could not measure
2469        // it, so its line has no coverage question to answer. Counting it as
2470        // covered would inflate the ratio with a line nothing verified.
2471        let point = |id: &str| PointMeta {
2472            id: id.into(),
2473            kind: PointKind::Statement,
2474            file: "src/app.js".into(),
2475            line: 1,
2476            column: 0,
2477            source: "work();".into(),
2478            label: None,
2479        };
2480        let manifest = CoverageManifest {
2481            decisions: vec![],
2482            points: vec![point("declined")],
2483            branches: vec![],
2484            limitations: vec![],
2485            unmeasured: vec!["declined".into()],
2486            scope: None,
2487        };
2488        let view = create_coverage_view(
2489            &manifest,
2490            &[raw("test", 0, "passed", &["declined"])],
2491            "time",
2492        )
2493        .unwrap();
2494
2495        assert_eq!(view.summary.lines.total, 0);
2496        assert_eq!(view.summary.lines.covered, 0);
2497        let line = view.lines.first().expect("the line stays addressable");
2498        assert!(!line.measured);
2499        assert!(!line.covered, "an unmeasured line is not covered either");
2500    }
2501
2502    #[test]
2503    fn report_retains_unexecuted_manifest_conditions() {
2504        let manifest = CoverageManifest {
2505            unmeasured: Vec::new(),
2506            decisions: vec![DecisionMeta {
2507                id: "decision".into(),
2508                file: "src/app.js".into(),
2509                line: 1,
2510                column: 0,
2511                source: "left && right".into(),
2512                conditions: vec!["left".into(), "right".into()],
2513                kind: "if".into(),
2514            }],
2515            points: vec![],
2516            branches: vec![],
2517            limitations: vec![],
2518            scope: None,
2519        };
2520        let view =
2521            create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
2522        assert_eq!(view.summary.conditions, 2);
2523        assert_eq!(view.summary.covered_conditions, 0);
2524        assert_eq!(view.decisions[0].conditions.len(), 2);
2525    }
2526
2527    #[test]
2528    fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
2529        let manifest = CoverageManifest {
2530            unmeasured: Vec::new(),
2531            decisions: vec![DecisionMeta {
2532                id: "application-decision".into(),
2533                file: "src/app.js".into(),
2534                line: 1,
2535                column: 0,
2536                source: "left && right".into(),
2537                conditions: vec!["left".into(), "right".into()],
2538                kind: "if".into(),
2539            }],
2540            points: vec![],
2541            branches: vec![],
2542            limitations: vec![],
2543            scope: None,
2544        };
2545        let mut attempt = raw("test", 0, "passed", &[]);
2546        attempt.runtime[0].decisions.push(DecisionSnapshot {
2547            meta: DecisionMeta {
2548                id: "synthetic-fixture".into(),
2549                file: "fixtures/generated.js".into(),
2550                line: 1,
2551                column: 0,
2552                source: "a && b && c".into(),
2553                conditions: vec!["a".into(), "b".into(), "c".into()],
2554                kind: "if".into(),
2555            },
2556            vectors: vec![McdcVector {
2557                values: vec![Some(true), Some(true), Some(true)],
2558                outcome: true,
2559            }],
2560        });
2561
2562        let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2563        assert_eq!(view.decisions.len(), 1);
2564        assert_eq!(view.decisions[0].meta.id, "application-decision");
2565        assert!(view.decisions[0].vectors.is_empty());
2566    }
2567
2568    #[test]
2569    fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
2570        let expected = DecisionMeta {
2571            id: "application-decision".into(),
2572            file: "src/app.js".into(),
2573            line: 1,
2574            column: 0,
2575            source: "left && right".into(),
2576            conditions: vec!["left".into(), "right".into()],
2577            kind: "if".into(),
2578        };
2579        let manifest = CoverageManifest {
2580            unmeasured: Vec::new(),
2581            decisions: vec![expected.clone()],
2582            points: vec![],
2583            branches: vec![],
2584            limitations: vec![],
2585            scope: Some(serde_json::json!({
2586                "entries": [{ "file": "src/empty.js", "status": "included" }]
2587            })),
2588        };
2589        let vector = McdcVector {
2590            values: vec![Some(true), Some(true)],
2591            outcome: true,
2592        };
2593
2594        let mut unknown = raw("test", 0, "passed", &[]);
2595        unknown.runtime[0].decisions.push(DecisionSnapshot {
2596            meta: DecisionMeta {
2597                id: "unknown-decision".into(),
2598                file: "src/empty.js".into(),
2599                ..expected.clone()
2600            },
2601            vectors: vec![vector.clone()],
2602        });
2603        assert!(matches!(
2604            create_coverage_view(&manifest, &[unknown], "time"),
2605            Err(ReportError::InvalidServerRecord(reason))
2606                if reason.contains("absent from the frozen manifest")
2607        ));
2608
2609        let mut changed = raw("test", 0, "passed", &[]);
2610        changed.runtime[0].decisions.push(DecisionSnapshot {
2611            meta: DecisionMeta {
2612                source: "left || right".into(),
2613                ..expected
2614            },
2615            vectors: vec![vector],
2616        });
2617        assert!(matches!(
2618            create_coverage_view(&manifest, &[changed], "time"),
2619            Err(ReportError::InvalidServerRecord(reason))
2620                if reason.contains("differs from the frozen manifest")
2621        ));
2622    }
2623
2624    #[test]
2625    fn timestamp_overlap_cannot_upgrade_assertion_confidence() {
2626        let manifest = CoverageManifest {
2627            unmeasured: Vec::new(),
2628            decisions: vec![],
2629            points: vec![point("hit", 1)],
2630            branches: vec![],
2631            limitations: vec![],
2632            scope: None,
2633        };
2634        let phase = CoveragePhase {
2635            id: "assertion".into(),
2636            kind: "assertion".into(),
2637            operation: "equal".into(),
2638            source: None,
2639            caused_by_phase_id: None,
2640            started_at_ms: 100,
2641            ended_at_ms: Some(120),
2642            status: Some("passed".into()),
2643            error: None,
2644        };
2645        let mut attempt = raw("test", 0, "passed", &["hit"]);
2646        attempt.phases.push(phase.clone());
2647        attempt.runtime[0].events.push(RuntimeEvent {
2648            event_type: "hit".into(),
2649            id: "hit".into(),
2650            vector: None,
2651            timestamp_ms: 110,
2652            phase_id: None,
2653            environment: "server".into(),
2654        });
2655        let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
2656        assert_eq!(inferred.points[0].confidence.level, "executed");
2657        assert!(!inferred.points[0].confidence.asserted);
2658
2659        attempt.runtime[0].events[0].phase_id = Some(phase.id);
2660        let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2661        assert_eq!(explicit.points[0].confidence.level, "asserted");
2662        assert!(explicit.points[0].confidence.asserted);
2663    }
2664
2665    #[test]
2666    fn verified_view_uses_only_the_terminal_successful_attempt() {
2667        let manifest = CoverageManifest {
2668            unmeasured: Vec::new(),
2669            decisions: vec![],
2670            points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
2671            branches: vec![],
2672            limitations: vec![],
2673            scope: None,
2674        };
2675        let failed = raw("flaky", 0, "failed", &["failed"]);
2676        let mut passed = raw("flaky", 1, "passed", &["passed"]);
2677        passed.flaky = true;
2678        let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
2679        expected.expected_status = Some("failed".into());
2680        let request = CoverageReportRequest {
2681            run_id: "run".into(),
2682            manifest,
2683            raw_results: vec![failed, passed, expected],
2684            generated_at: "time".into(),
2685            coverage_model: None,
2686            integrity: None,
2687            test_exit_code: ExitCodeInput::Missing,
2688        };
2689        let report = analyze_coverage_results(&request).unwrap();
2690        assert!(!report.filters.passed.points[0].covered);
2691        assert!(report.filters.passed.points[1].covered);
2692        assert!(!report.filters.passed.points[2].covered);
2693        assert!(report.filters.failed.points[0].covered);
2694        assert_eq!(
2695            report
2696                .view
2697                .tests
2698                .iter()
2699                .find(|test| test.name == "expected-failure")
2700                .unwrap()
2701                .outcome,
2702            "failed"
2703        );
2704        assert_eq!(report.view.tests[1].outcome, "flaky");
2705    }
2706
2707    #[test]
2708    fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
2709        let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
2710        expected.expected_status = Some("failed".into());
2711        let companion = raw("expected-failure", 0, "failed", &["expected"]);
2712        let request = CoverageReportRequest {
2713            run_id: "run".into(),
2714            manifest: CoverageManifest {
2715                unmeasured: Vec::new(),
2716                decisions: vec![],
2717                points: vec![point("expected", 1)],
2718                branches: vec![],
2719                limitations: vec![],
2720                scope: None,
2721            },
2722            raw_results: vec![companion, expected],
2723            generated_at: "time".into(),
2724            coverage_model: None,
2725            integrity: None,
2726            test_exit_code: ExitCodeInput::Missing,
2727        };
2728        let report = analyze_coverage_results(&request).unwrap();
2729        assert_eq!(report.view.tests[0].outcome, "passed");
2730        assert!(!report.filters.passed.points[0].covered);
2731        assert!(!report.filters.failed.points[0].covered);
2732    }
2733
2734    #[test]
2735    fn selected_but_unstarted_test_is_not_an_invented_attempt() {
2736        let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
2737        unstarted.scope = None;
2738        unstarted.retry = None;
2739        unstarted.runtime.clear();
2740        let request = CoverageReportRequest {
2741            run_id: "run".into(),
2742            manifest: CoverageManifest {
2743                unmeasured: Vec::new(),
2744                decisions: vec![],
2745                points: vec![],
2746                branches: vec![],
2747                limitations: vec![],
2748                scope: None,
2749            },
2750            raw_results: vec![unstarted],
2751            generated_at: "time".into(),
2752            coverage_model: None,
2753            integrity: None,
2754            test_exit_code: ExitCodeInput::Present(Some(100)),
2755        };
2756        let report = analyze_coverage_results(&request).unwrap();
2757        assert_eq!(report.view.tests[0].outcome, "unstarted");
2758        assert!(report.view.tests[0].attempts.is_empty());
2759        assert!(report.filters.passed.tests.is_empty());
2760        assert!(report.filters.failed.tests.is_empty());
2761    }
2762
2763    fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
2764        let nonce = SystemTime::now()
2765            .duration_since(SystemTime::UNIX_EPOCH)
2766            .unwrap()
2767            .as_nanos();
2768        let root = std::env::temp_dir().join(format!(
2769            "supercov-rust-report-{}-{nonce}-{}",
2770            std::process::id(),
2771            ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
2772        ));
2773        fs::create_dir_all(&root).unwrap();
2774        let path = root.join("evidence.raw.gz");
2775        if !entries
2776            .iter()
2777            .any(|entry| entry.path == "coverage-model.json")
2778        {
2779            entries.push(EvidenceArchiveEntry {
2780                path: "coverage-model.json".into(),
2781                contents: serde_json::to_vec(
2782                    &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
2783                        .unwrap(),
2784                )
2785                .unwrap(),
2786            });
2787        }
2788        if !entries.iter().any(|entry| entry.path == "frontend.json") {
2789            entries.push(EvidenceArchiveEntry {
2790                path: "frontend.json".into(),
2791                contents: serde_json::to_vec(&serde_json::json!({
2792                    "protocolVersion": 2,
2793                    "frontendId": "javascript",
2794                    "frontendVersion": "fixture-v1",
2795                    "language": "javascript",
2796                    "structuralSource": "owned-probes",
2797                    "runners": [{
2798                        "runner": "node:test",
2799                        "executionModel": "serial-in-process",
2800                        "attribution": {
2801                            "run": "exact",
2802                            "worker": "unavailable",
2803                            "test": "exact",
2804                            "retry": "exact",
2805                            "phase": "exact",
2806                            "action": "exact",
2807                            "assertion": "exact"
2808                        },
2809                        "limitations": [{
2810                            "id": "fixture-worker-unavailable",
2811                            "scopes": ["worker"],
2812                            "reason": "The fixture does not require worker identity"
2813                        }]
2814                    }],
2815                    "structuralLimitations": []
2816                }))
2817                .unwrap(),
2818            });
2819        }
2820        write_archive(entries, &path).unwrap();
2821        path
2822    }
2823
2824    #[test]
2825    fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
2826        let manifest = CoverageManifest {
2827            unmeasured: Vec::new(),
2828            decisions: vec![],
2829            points: vec![point("background-hit", 1), point("test-hit", 2)],
2830            branches: vec![],
2831            limitations: vec![],
2832            scope: None,
2833        };
2834        // Declare exactly the observed runners: the journal's node:test and the
2835        // synthesized background runner, shaped as the JavaScript run declares it.
2836        let unattributed = |axis: &str| {
2837            serde_json::json!({
2838                "id": format!("background-no-{axis}"),
2839                "scopes": [axis],
2840                "reason": format!("Runner background did not expose exact {axis} identity for every result")
2841            })
2842        };
2843        let frontend = serde_json::json!({
2844            "protocolVersion": 2,
2845            "frontendId": "javascript",
2846            "frontendVersion": "fixture-v1",
2847            "language": "javascript",
2848            "structuralSource": "owned-probes",
2849            "runners": [
2850                {
2851                    "runner": "node:test",
2852                    "executionModel": "serial-in-process",
2853                    "attribution": {
2854                        "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2855                        "phase": "exact", "action": "exact", "assertion": "exact"
2856                    },
2857                    "limitations": [{
2858                        "id": "fixture-worker-unavailable",
2859                        "scopes": ["worker"],
2860                        "reason": "The fixture does not require worker identity"
2861                    }]
2862                },
2863                {
2864                    "runner": "background",
2865                    "executionModel": "parallel-unattributed",
2866                    "attribution": {
2867                        "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2868                        "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2869                    },
2870                    "limitations": [
2871                        unattributed("worker"), unattributed("test"), unattributed("retry"),
2872                        unattributed("phase"), unattributed("action"), unattributed("assertion")
2873                    ]
2874                }
2875            ],
2876            "structuralLimitations": []
2877        });
2878        let path = archive(vec![
2879            EvidenceArchiveEntry {
2880                path: "manifest.json".into(),
2881                contents: serde_json::to_vec(&manifest).unwrap(),
2882            },
2883            EvidenceArchiveEntry {
2884                path: "frontend.json".into(),
2885                contents: serde_json::to_vec(&frontend).unwrap(),
2886            },
2887            EvidenceArchiveEntry {
2888                path: "playwright-worker-1.mcdc.jsonl".into(),
2889                contents: {
2890                    let mut contents =
2891                        serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2892                            .unwrap();
2893                    contents.extend_from_slice(b"\npartial-final-line");
2894                    contents
2895                },
2896            },
2897            EvidenceArchiveEntry {
2898                path: "server/background/worker.jsonl".into(),
2899                contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
2900            },
2901        ]);
2902        let result = analyze_coverage_archive(&ArchiveReportRequest {
2903            archive_path: path.clone(),
2904            run_id: "run".into(),
2905            generated_at: "time".into(),
2906            integrity: None,
2907            test_exit_code: ExitCodeInput::Missing,
2908        });
2909        assert!(matches!(
2910            result,
2911            Err(ReportError::InvalidJson { path, .. })
2912                if path == "playwright-worker-1.mcdc.jsonl"
2913        ));
2914        fs::remove_dir_all(path.parent().unwrap()).unwrap();
2915    }
2916
2917    #[test]
2918    fn a_torn_background_record_is_counted_not_fatal() {
2919        // Pool VMs restored from one snapshot are clones with the same pid and
2920        // cached shard path; their appends over a shared mount tear lines. One
2921        // torn line is one lost record: the report must still build, count it,
2922        // and surface it through the corrupt-evidence accounting.
2923        let manifest = CoverageManifest {
2924            unmeasured: Vec::new(),
2925            decisions: vec![],
2926            points: vec![],
2927            branches: vec![],
2928            limitations: vec![],
2929            scope: None,
2930        };
2931        // Declare exactly the observed runners: the journal's node:test and the
2932        // synthesized background runner, shaped as the JavaScript run declares it.
2933        let unattributed = |axis: &str| {
2934            serde_json::json!({
2935                "id": format!("background-no-{axis}"),
2936                "scopes": [axis],
2937                "reason": format!("Runner background did not expose exact {axis} identity for every result")
2938            })
2939        };
2940        let frontend = serde_json::json!({
2941            "protocolVersion": 2,
2942            "frontendId": "javascript",
2943            "frontendVersion": "fixture-v1",
2944            "language": "javascript",
2945            "structuralSource": "owned-probes",
2946            "runners": [
2947                {
2948                    "runner": "node:test",
2949                    "executionModel": "serial-in-process",
2950                    "attribution": {
2951                        "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2952                        "phase": "exact", "action": "exact", "assertion": "exact"
2953                    },
2954                    "limitations": [{
2955                        "id": "fixture-worker-unavailable",
2956                        "scopes": ["worker"],
2957                        "reason": "The fixture does not require worker identity"
2958                    }]
2959                },
2960                {
2961                    "runner": "background",
2962                    "executionModel": "parallel-unattributed",
2963                    "attribution": {
2964                        "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2965                        "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2966                    },
2967                    "limitations": [
2968                        unattributed("worker"), unattributed("test"), unattributed("retry"),
2969                        unattributed("phase"), unattributed("action"), unattributed("assertion")
2970                    ]
2971                }
2972            ],
2973            "structuralLimitations": []
2974        });
2975        let path = archive(vec![
2976            EvidenceArchiveEntry {
2977                path: "manifest.json".into(),
2978                contents: serde_json::to_vec(&manifest).unwrap(),
2979            },
2980            EvidenceArchiveEntry {
2981                path: "frontend.json".into(),
2982                contents: serde_json::to_vec(&frontend).unwrap(),
2983            },
2984            EvidenceArchiveEntry {
2985                path: "playwright-worker-1.mcdc.jsonl".into(),
2986                contents: {
2987                    let mut contents =
2988                        serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2989                            .unwrap();
2990                    contents.push(b'\n');
2991                    contents
2992                },
2993            },
2994            EvidenceArchiveEntry {
2995                path: "execution.37360-1.168.jsonl".into(),
2996                contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
2997            },
2998            EvidenceArchiveEntry {
2999                path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
3000                contents: concat!(
3001                    "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
3002                    "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
3003                    "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
3004                )
3005                .as_bytes()
3006                .to_vec(),
3007            },
3008        ]);
3009        let report = analyze_coverage_archive(&ArchiveReportRequest {
3010            archive_path: path.clone(),
3011            run_id: "run".into(),
3012            generated_at: "time".into(),
3013            integrity: None,
3014            test_exit_code: ExitCodeInput::Missing,
3015        })
3016        .expect("a torn evidence line must not fail the whole run");
3017        let transport = report.view.transport.expect("transport stats");
3018        assert_eq!(transport.corrupt_records, 2);
3019        assert_eq!(transport.corrupt_files, 2);
3020        assert_eq!(transport.background_server_records, 2);
3021        fs::remove_dir_all(path.parent().unwrap()).unwrap();
3022    }
3023
3024    #[test]
3025    fn archive_analysis_rejects_cross_run_evidence() {
3026        let manifest = CoverageManifest {
3027            unmeasured: Vec::new(),
3028            decisions: vec![],
3029            points: vec![],
3030            branches: vec![],
3031            limitations: vec![],
3032            scope: None,
3033        };
3034        let mut result = raw("test", 0, "passed", &[]);
3035        result.scope = Some(ExecutionScope {
3036            version: 1,
3037            run_id: "other-run".into(),
3038            worker_id: "worker".into(),
3039            test_id: "test".into(),
3040            test_key: "key".into(),
3041            retry: 0,
3042            attempt_id: "attempt".into(),
3043        });
3044        let path = archive(vec![
3045            EvidenceArchiveEntry {
3046                path: "manifest.json".into(),
3047                contents: serde_json::to_vec(&manifest).unwrap(),
3048            },
3049            EvidenceArchiveEntry {
3050                path: "worker/mcdc.json".into(),
3051                contents: serde_json::to_vec(&result).unwrap(),
3052            },
3053        ]);
3054        assert!(matches!(
3055            analyze_coverage_archive(&ArchiveReportRequest {
3056                archive_path: path.clone(),
3057                run_id: "run".into(),
3058                generated_at: "time".into(),
3059                integrity: None,
3060                test_exit_code: ExitCodeInput::Missing,
3061            }),
3062            Err(ReportError::InvalidArchive(reason))
3063                if reason.contains("expected=run actual=other-run")
3064        ));
3065        fs::remove_dir_all(path.parent().unwrap()).unwrap();
3066    }
3067
3068    #[test]
3069    fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
3070        let vectors: Value = serde_json::from_str(include_str!(
3071            "../test-assets/coverage-model-v1/vectors.json"
3072        ))
3073        .unwrap();
3074        for value in vectors["valid"].as_array().unwrap() {
3075            let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
3076            model.into_declaration().unwrap();
3077        }
3078        for vector in vectors["invalid"].as_array().unwrap() {
3079            let value = vector["value"].clone();
3080            if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
3081                assert!(
3082                    model.into_declaration().is_err(),
3083                    "accepted invalid model vector: {}",
3084                    vector["reason"]
3085                );
3086            }
3087        }
3088
3089        let manifest = CoverageManifest {
3090            unmeasured: Vec::new(),
3091            decisions: vec![],
3092            points: vec![],
3093            branches: vec![],
3094            limitations: vec![],
3095            scope: None,
3096        };
3097        let path = archive(vec![
3098            EvidenceArchiveEntry {
3099                path: "coverage-model.json".into(),
3100                contents: serde_json::to_vec(&PersistedCoverageModel {
3101                    schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
3102                    language: "rust".into(),
3103                    variant: "rust-source-v1".into(),
3104                    name: "Rust source coverage".into(),
3105                    completeness_meaning: "Every Rust obligation was satisfied.".into(),
3106                    measured: vec!["Rust statements".into()],
3107                    not_measured: vec![],
3108                })
3109                .unwrap(),
3110            },
3111            EvidenceArchiveEntry {
3112                path: "manifest.json".into(),
3113                contents: serde_json::to_vec(&manifest).unwrap(),
3114            },
3115        ]);
3116        assert!(matches!(
3117            analyze_coverage_archive(&ArchiveReportRequest {
3118                archive_path: path.clone(),
3119                run_id: "run".into(),
3120                generated_at: "time".into(),
3121                integrity: None,
3122                test_exit_code: ExitCodeInput::Missing,
3123            }),
3124            Err(ReportError::InvalidArchive(reason))
3125                if reason.contains("javascript differs from coverage model language rust")
3126        ));
3127        fs::remove_dir_all(path.parent().unwrap()).unwrap();
3128    }
3129
3130    #[test]
3131    fn rust_compiler_scope_requires_an_exact_full_source_fingerprint() {
3132        let mut manifest = CoverageManifest {
3133            unmeasured: Vec::new(),
3134            decisions: vec![],
3135            points: vec![],
3136            branches: vec![],
3137            limitations: vec![],
3138            scope: Some(serde_json::json!({
3139                "language": "rust",
3140                "model": "rust-source-v1",
3141                "crate": "fixture",
3142                "measurementComplete": false,
3143                "sourceFingerprint": {
3144                    "algorithm": "sha256",
3145                    "digest": "1".repeat(64),
3146                    "files": 2,
3147                    "generatedFiles": 1,
3148                },
3149            })),
3150        };
3151        validate_rust_compiler_scope(&manifest).unwrap();
3152
3153        manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3154            Value::String("not-a-digest".into());
3155        assert!(matches!(
3156            validate_rust_compiler_scope(&manifest),
3157            Err(ReportError::InvalidArchive(reason))
3158                if reason == "malformed Rust compiler source fingerprint"
3159        ));
3160
3161        manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3162            Value::String("1".repeat(64));
3163        manifest.scope.as_mut().unwrap()["sourceFingerprint"]["unexpected"] = Value::Bool(true);
3164        assert!(validate_rust_compiler_scope(&manifest).is_err());
3165    }
3166
3167    #[test]
3168    fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
3169        let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
3170            "runId": "run",
3171            "manifest": { "decisions": [], "points": [], "branches": [] },
3172            "rawResults": [{
3173                "test": "test",
3174                "status": "passed",
3175                "browser": [],
3176                "server": []
3177            }],
3178            "generatedAt": "time",
3179            "testExitCode": null
3180        }))
3181        .unwrap();
3182        let report = analyze_coverage_results(&request).unwrap();
3183        assert_eq!(
3184            report.execution,
3185            Some(ExecutionResult {
3186                test_exit_code: None,
3187                valid: false,
3188            })
3189        );
3190    }
3191}