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
1060fn confidence_for(
1061    test_ids: impl IntoIterator<Item = String>,
1062    phase_ids: impl IntoIterator<Item = String>,
1063    explicit_phase_ids: impl IntoIterator<Item = String>,
1064    tests: &HashMap<String, MutableTest>,
1065    phases: &HashMap<String, MutablePhase>,
1066    asserted_phase_ids: &BTreeSet<String>,
1067) -> CoverageConfidence {
1068    let test_ids = test_ids.into_iter().collect::<BTreeSet<_>>();
1069    let phase_ids = phase_ids.into_iter().collect::<BTreeSet<_>>();
1070    let explicit_phase_ids = explicit_phase_ids.into_iter().collect::<BTreeSet<_>>();
1071    let asserted_phases = explicit_phase_ids
1072        .iter()
1073        .filter(|id| asserted_phase_ids.contains(*id))
1074        .collect::<Vec<_>>();
1075    let asserted_tests = asserted_phases
1076        .iter()
1077        .filter_map(|id| phases.get(*id).map(|phase| phase.test.clone()))
1078        .collect::<BTreeSet<_>>();
1079    let provenances = test_ids
1080        .iter()
1081        .filter_map(|id| tests.get(id).map(|test| &test.provenance))
1082        .collect::<Vec<_>>();
1083    let roles = test_ids
1084        .iter()
1085        .filter_map(|id| tests.get(id).map(|test| test.role.as_str()))
1086        .collect::<Vec<_>>();
1087    let phase_kinds = phase_ids
1088        .iter()
1089        .filter_map(|id| phases.get(id).map(|phase| phase.phase.kind.as_str()))
1090        .collect::<Vec<_>>();
1091    let only = |phase_kind: &str, role: &str| {
1092        if phase_kinds.is_empty() {
1093            !roles.is_empty() && roles.iter().all(|value| *value == role)
1094        } else {
1095            phase_kinds.iter().all(|value| *value == phase_kind)
1096        }
1097    };
1098    let has_action = explicit_phase_ids.iter().any(|id| {
1099        phases
1100            .get(id)
1101            .is_some_and(|phase| phase.phase.kind == "action")
1102    });
1103    let level = if test_ids.is_empty() {
1104        "unexecuted"
1105    } else if !asserted_tests.is_empty() {
1106        "asserted"
1107    } else if has_action {
1108        "action"
1109    } else {
1110        "executed"
1111    };
1112    let runners = provenances
1113        .iter()
1114        .map(|provenance| provenance.runner.clone())
1115        .collect::<BTreeSet<_>>();
1116    let kinds = provenances
1117        .iter()
1118        .map(|provenance| provenance.kind.clone())
1119        .collect::<BTreeSet<_>>();
1120    CoverageConfidence {
1121        level: level.into(),
1122        setup_only: only("setup", "setup"),
1123        background_only: only("background", "background"),
1124        asserted: !asserted_tests.is_empty(),
1125        tests: sorted(&test_ids),
1126        asserted_tests: sorted(&asserted_tests),
1127        runners: sorted(&runners),
1128        e2e: kinds.contains("e2e"),
1129        kinds: sorted(&kinds),
1130    }
1131}
1132
1133fn add_reference(map: &mut HashMap<String, BTreeSet<String>>, id: &str, value: &str) {
1134    map.entry(id.into()).or_default().insert(value.into());
1135}
1136
1137pub fn create_coverage_view(
1138    manifest: &CoverageManifest,
1139    raw_results: &[RawTestResult],
1140    generated_at: &str,
1141) -> Result<CoverageView, ReportError> {
1142    create_coverage_view_with_model(
1143        manifest,
1144        raw_results,
1145        generated_at,
1146        &javascript_coverage_model(),
1147    )
1148}
1149
1150fn create_coverage_view_with_model(
1151    manifest: &CoverageManifest,
1152    raw_results: &[RawTestResult],
1153    generated_at: &str,
1154    coverage_model: &CoverageModelDeclaration,
1155) -> Result<CoverageView, ReportError> {
1156    let mut decision_metadata = manifest.decisions.clone();
1157    let decision_indexes = decision_metadata
1158        .iter()
1159        .enumerate()
1160        .map(|(index, meta)| (meta.id.clone(), index))
1161        .collect::<HashMap<_, _>>();
1162    let manifest_files = manifest
1163        .decisions
1164        .iter()
1165        .map(|meta| meta.file.clone())
1166        .chain(manifest.points.iter().map(|meta| meta.file.clone()))
1167        .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
1168        .chain(
1169            manifest
1170                .scope
1171                .as_ref()
1172                .and_then(|scope| scope.get("entries"))
1173                .and_then(Value::as_array)
1174                .into_iter()
1175                .flatten()
1176                .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
1177                .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
1178        )
1179        .collect::<BTreeSet<_>>();
1180    let mut vectors_by_decision = HashMap::<String, Vec<MutableObservation>>::new();
1181    let mut vector_indexes = HashMap::<String, HashMap<String, usize>>::new();
1182    let mut tests_by_decision = HashMap::<String, BTreeSet<String>>::new();
1183    let mut tests_by_hit = HashMap::<String, BTreeSet<String>>::new();
1184    let mut tests_by_id = HashMap::<String, MutableTest>::new();
1185    let mut test_order = Vec::<String>::new();
1186    let mut phases_by_id = HashMap::<String, MutablePhase>::new();
1187    let mut phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1188    let mut explicit_phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1189
1190    for raw in raw_results {
1191        let id = raw_test_id(raw).to_owned();
1192        if !tests_by_id.contains_key(&id) {
1193            test_order.push(id.clone());
1194            tests_by_id.insert(
1195                id.clone(),
1196                MutableTest {
1197                    id: id.clone(),
1198                    name: raw.test.clone(),
1199                    file: raw.test_file.clone(),
1200                    title: raw.title.clone(),
1201                    retries: raw.retry.into_iter().collect(),
1202                    attempts: BTreeMap::new(),
1203                    unstarted: raw.status.as_deref() == Some("unstarted"),
1204                    runner_reported_flaky: raw.flaky,
1205                    provenance: raw.provenance.clone(),
1206                    role: raw.role.clone(),
1207                    hits: BTreeSet::new(),
1208                    decisions: BTreeMap::new(),
1209                },
1210            );
1211        }
1212        let test = tests_by_id.get_mut(&id).expect("test was inserted");
1213        test.unstarted |= raw.status.as_deref() == Some("unstarted");
1214        if let Some(retry) = raw.retry {
1215            test.retries.insert(retry);
1216        }
1217        test.runner_reported_flaky |= raw.flaky;
1218        record_attempt(test, raw);
1219
1220        let mut ordered_phases = raw.phases.clone();
1221        ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1222        for phase in &ordered_phases {
1223            phases_by_id.insert(
1224                phase.id.clone(),
1225                MutablePhase {
1226                    phase: phase.clone(),
1227                    test: id.clone(),
1228                    hits: BTreeSet::new(),
1229                    decisions: BTreeMap::new(),
1230                    browser_events: 0,
1231                    server_events: 0,
1232                    explicit_events: 0,
1233                    inferred_events: 0,
1234                    explicit_browser_events: 0,
1235                    inferred_browser_events: 0,
1236                    explicit_server_events: 0,
1237                    inferred_server_events: 0,
1238                },
1239            );
1240        }
1241
1242        let correlate = |event: &RuntimeEvent| {
1243            event.phase_id.clone().or_else(|| {
1244                ordered_phases
1245                    .iter()
1246                    .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1247                    .last()
1248                    .map(|phase| phase.id.clone())
1249            })
1250        };
1251
1252        let snapshots = raw.runtime.iter().chain(&raw.browser);
1253        for snapshot in snapshots {
1254            for decision in &snapshot.decisions {
1255                let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1256                    if manifest_files.contains(&decision.meta.file) {
1257                        return Err(ReportError::InvalidServerRecord(format!(
1258                            "decision {} is absent from the frozen manifest",
1259                            decision.meta.id
1260                        )));
1261                    }
1262                    continue;
1263                };
1264                if decision_metadata[index] != decision.meta {
1265                    return Err(ReportError::InvalidServerRecord(format!(
1266                        "decision {} metadata differs from the frozen manifest",
1267                        decision.meta.id
1268                    )));
1269                }
1270                for vector in &decision.vectors {
1271                    let key = vector_key(vector);
1272                    let indexes = vector_indexes.entry(decision.meta.id.clone()).or_default();
1273                    let observations = vectors_by_decision
1274                        .entry(decision.meta.id.clone())
1275                        .or_default();
1276                    let observation_index = *indexes.entry(key.clone()).or_insert_with(|| {
1277                        observations.push(MutableObservation {
1278                            vector: vector.clone(),
1279                            tests: BTreeSet::new(),
1280                            phases: BTreeSet::new(),
1281                            explicit_phases: BTreeSet::new(),
1282                        });
1283                        observations.len() - 1
1284                    });
1285                    observations[observation_index].tests.insert(id.clone());
1286                    tests_by_id
1287                        .get_mut(&id)
1288                        .expect("registered test")
1289                        .decisions
1290                        .entry(decision.meta.id.clone())
1291                        .or_default()
1292                        .insert(vector);
1293                }
1294                if !decision.vectors.is_empty() {
1295                    add_reference(&mut tests_by_decision, &decision.meta.id, &id);
1296                }
1297            }
1298            for hit in &snapshot.hits {
1299                add_reference(&mut tests_by_hit, hit, &id);
1300                tests_by_id
1301                    .get_mut(&id)
1302                    .expect("registered test")
1303                    .hits
1304                    .insert(hit.clone());
1305            }
1306            for event in &snapshot.events {
1307                let explicit = event.phase_id.is_some();
1308                let Some(phase_id) = correlate(event) else {
1309                    continue;
1310                };
1311                let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1312                    continue;
1313                };
1314                if event.environment == "browser" {
1315                    phase.browser_events += 1;
1316                    if explicit {
1317                        phase.explicit_browser_events += 1;
1318                    } else {
1319                        phase.inferred_browser_events += 1;
1320                    }
1321                } else {
1322                    phase.server_events += 1;
1323                    if explicit {
1324                        phase.explicit_server_events += 1;
1325                    } else {
1326                        phase.inferred_server_events += 1;
1327                    }
1328                }
1329                if explicit {
1330                    phase.explicit_events += 1;
1331                } else {
1332                    phase.inferred_events += 1;
1333                }
1334                if event.event_type == "hit" {
1335                    phase.hits.insert(event.id.clone());
1336                    add_reference(&mut phases_by_hit, &event.id, &phase_id);
1337                    if explicit {
1338                        add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1339                    }
1340                } else if event.event_type == "decision" {
1341                    let vector = event
1342                        .vector
1343                        .as_ref()
1344                        .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1345                    phase
1346                        .decisions
1347                        .entry(event.id.clone())
1348                        .or_default()
1349                        .insert(vector);
1350                    if let Some(index) = vector_indexes
1351                        .get(&event.id)
1352                        .and_then(|indexes| indexes.get(&vector_key(vector)))
1353                        .copied()
1354                        && let Some(observation) = vectors_by_decision
1355                            .get_mut(&event.id)
1356                            .and_then(|observations| observations.get_mut(index))
1357                    {
1358                        observation.phases.insert(phase_id.clone());
1359                        if explicit {
1360                            observation.explicit_phases.insert(phase_id.clone());
1361                        }
1362                    }
1363                } else {
1364                    return Err(ReportError::InvalidEvent(event.event_type.clone()));
1365                }
1366            }
1367        }
1368
1369        for record in &raw.server {
1370            let (record_id, decision) = if record.record_type == "decision" {
1371                let meta = record
1372                    .meta
1373                    .as_ref()
1374                    .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1375                let vector = record
1376                    .vector
1377                    .as_ref()
1378                    .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1379                let Some(index) = decision_indexes.get(&meta.id).copied() else {
1380                    if manifest_files.contains(&meta.file) {
1381                        return Err(ReportError::InvalidServerRecord(format!(
1382                            "decision {} is absent from the frozen manifest",
1383                            meta.id
1384                        )));
1385                    }
1386                    continue;
1387                };
1388                if decision_metadata[index] != *meta {
1389                    return Err(ReportError::InvalidServerRecord(format!(
1390                        "decision {} metadata differs from the frozen manifest",
1391                        meta.id
1392                    )));
1393                }
1394                let key = vector_key(vector);
1395                let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1396                let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1397                let index = *indexes.entry(key).or_insert_with(|| {
1398                    observations.push(MutableObservation {
1399                        vector: vector.clone(),
1400                        tests: BTreeSet::new(),
1401                        phases: BTreeSet::new(),
1402                        explicit_phases: BTreeSet::new(),
1403                    });
1404                    observations.len() - 1
1405                });
1406                observations[index].tests.insert(id.clone());
1407                tests_by_id
1408                    .get_mut(&id)
1409                    .expect("registered test")
1410                    .decisions
1411                    .entry(meta.id.clone())
1412                    .or_default()
1413                    .insert(vector);
1414                add_reference(&mut tests_by_decision, &meta.id, &id);
1415                (meta.id.clone(), Some(vector.clone()))
1416            } else if record.record_type == "hit" {
1417                let hit = record
1418                    .id
1419                    .as_ref()
1420                    .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1421                add_reference(&mut tests_by_hit, hit, &id);
1422                tests_by_id
1423                    .get_mut(&id)
1424                    .expect("registered test")
1425                    .hits
1426                    .insert(hit.clone());
1427                (hit.clone(), None)
1428            } else {
1429                return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1430            };
1431            let Some(timestamp_ms) = record.timestamp_ms else {
1432                continue;
1433            };
1434            let event = RuntimeEvent {
1435                event_type: record.record_type.clone(),
1436                id: record_id,
1437                vector: decision,
1438                timestamp_ms,
1439                phase_id: record.phase_id.clone(),
1440                environment: "server".into(),
1441            };
1442            let explicit = event.phase_id.is_some();
1443            let phase_id = correlate(&event);
1444            let Some(phase_id) = phase_id else { continue };
1445            let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1446                continue;
1447            };
1448            phase.server_events += 1;
1449            if explicit {
1450                phase.explicit_events += 1;
1451                phase.explicit_server_events += 1;
1452            } else {
1453                phase.inferred_events += 1;
1454                phase.inferred_server_events += 1;
1455            }
1456            if event.event_type == "hit" {
1457                phase.hits.insert(event.id.clone());
1458                add_reference(&mut phases_by_hit, &event.id, &phase_id);
1459                if explicit {
1460                    add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1461                }
1462            } else if let Some(vector) = &event.vector {
1463                phase
1464                    .decisions
1465                    .entry(event.id.clone())
1466                    .or_default()
1467                    .insert(vector);
1468                if let Some(index) = vector_indexes
1469                    .get(&event.id)
1470                    .and_then(|indexes| indexes.get(&vector_key(vector)))
1471                    .copied()
1472                    && let Some(observation) = vectors_by_decision
1473                        .get_mut(&event.id)
1474                        .and_then(|observations| observations.get_mut(index))
1475                {
1476                    observation.phases.insert(phase_id.clone());
1477                    if explicit {
1478                        observation.explicit_phases.insert(phase_id.clone());
1479                    }
1480                }
1481            }
1482        }
1483    }
1484
1485    let mut asserted_phase_ids = BTreeSet::new();
1486    for phase in phases_by_id.values() {
1487        if phase.phase.kind == "assertion" && phase.phase.status.as_deref() == Some("passed") {
1488            asserted_phase_ids.insert(phase.phase.id.clone());
1489            if let Some(cause) = &phase.phase.caused_by_phase_id {
1490                asserted_phase_ids.insert(cause.clone());
1491            }
1492        }
1493    }
1494
1495    decision_metadata.sort_by(|left, right| {
1496        left.file
1497            .cmp(&right.file)
1498            .then(left.line.cmp(&right.line))
1499            .then(left.column.cmp(&right.column))
1500    });
1501    let mut decisions = Vec::with_capacity(decision_metadata.len());
1502    for meta in decision_metadata {
1503        let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
1504        let mut observations = Vec::with_capacity(mutable.len());
1505        for observation in mutable {
1506            let confidence = confidence_for(
1507                sorted(&observation.tests),
1508                sorted(&observation.phases),
1509                sorted(&observation.explicit_phases),
1510                &tests_by_id,
1511                &phases_by_id,
1512                &asserted_phase_ids,
1513            );
1514            observations.push(VectorObservation {
1515                vector: observation.vector,
1516                tests: sorted(&observation.tests),
1517                phases: sorted(&observation.phases),
1518                explicit_phases: sorted(&observation.explicit_phases),
1519                confidence,
1520            });
1521        }
1522        let vectors = observations
1523            .iter()
1524            .map(|observation| observation.vector.clone())
1525            .collect::<Vec<_>>();
1526        let witnesses =
1527            find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
1528                ReportError::DecisionAnalysis {
1529                    decision_id: meta.id.clone(),
1530                    error,
1531                }
1532            })?;
1533        let mut conditions = Vec::with_capacity(meta.conditions.len());
1534        for (index, source) in meta.conditions.iter().enumerate() {
1535            let witness = witnesses[index].map(|witness| {
1536                [
1537                    vectors[witness.first].clone(),
1538                    vectors[witness.second].clone(),
1539                ]
1540            });
1541            let witness_tests = witnesses[index].map(|witness| {
1542                [
1543                    observations[witness.first].tests.clone(),
1544                    observations[witness.second].tests.clone(),
1545                ]
1546            });
1547            let assertion_covered = (0..observations.len()).any(|left| {
1548                ((left + 1)..observations.len()).any(|right| {
1549                    observations[left].confidence.asserted
1550                        && observations[right].confidence.asserted
1551                        && crate::coverage_analysis::is_independence_pair(
1552                            &observations[left].vector,
1553                            &observations[right].vector,
1554                            index,
1555                        )
1556                })
1557            });
1558            conditions.push(ConditionResult {
1559                index,
1560                source: source.clone(),
1561                covered: witness.is_some(),
1562                assertion_covered,
1563                witness,
1564                witness_tests,
1565            });
1566        }
1567        let decision_tests = tests_by_decision.remove(&meta.id).unwrap_or_default();
1568        let confidence = confidence_for(
1569            sorted(&decision_tests),
1570            observations
1571                .iter()
1572                .flat_map(|observation| observation.phases.clone()),
1573            observations
1574                .iter()
1575                .flat_map(|observation| observation.explicit_phases.clone()),
1576            &tests_by_id,
1577            &phases_by_id,
1578            &asserted_phase_ids,
1579        );
1580        decisions.push(DecisionResult {
1581            executed: !vectors.is_empty(),
1582            covered: conditions.iter().all(|condition| condition.covered),
1583            meta,
1584            vectors,
1585            vector_observations: observations,
1586            conditions,
1587            tests: sorted(&decision_tests),
1588            confidence,
1589        });
1590    }
1591
1592    let points = manifest
1593        .points
1594        .iter()
1595        .cloned()
1596        .map(|meta| {
1597            let tests = tests_by_hit.get(&meta.id).cloned().unwrap_or_default();
1598            let phases = phases_by_hit.get(&meta.id).cloned().unwrap_or_default();
1599            let explicit = explicit_phases_by_hit
1600                .get(&meta.id)
1601                .cloned()
1602                .unwrap_or_default();
1603            PointResult {
1604                covered: tests_by_hit.contains_key(&meta.id),
1605                confidence: confidence_for(
1606                    sorted(&tests),
1607                    sorted(&phases),
1608                    sorted(&explicit),
1609                    &tests_by_id,
1610                    &phases_by_id,
1611                    &asserted_phase_ids,
1612                ),
1613                meta,
1614                tests: sorted(&tests),
1615                phases: sorted(&phases),
1616            }
1617        })
1618        .collect::<Vec<_>>();
1619
1620    let branches = manifest
1621        .branches
1622        .iter()
1623        .cloned()
1624        .map(|meta| {
1625            let alternatives = meta
1626                .alternatives
1627                .iter()
1628                .map(|alternative| {
1629                    let tests = tests_by_hit
1630                        .get(&alternative.id)
1631                        .cloned()
1632                        .unwrap_or_default();
1633                    let phases = phases_by_hit
1634                        .get(&alternative.id)
1635                        .cloned()
1636                        .unwrap_or_default();
1637                    let explicit = explicit_phases_by_hit
1638                        .get(&alternative.id)
1639                        .cloned()
1640                        .unwrap_or_default();
1641                    AlternativeResult {
1642                        id: alternative.id.clone(),
1643                        label: alternative.label.clone(),
1644                        covered: tests_by_hit.contains_key(&alternative.id),
1645                        tests: sorted(&tests),
1646                        phases: sorted(&phases),
1647                        confidence: confidence_for(
1648                            sorted(&tests),
1649                            sorted(&phases),
1650                            sorted(&explicit),
1651                            &tests_by_id,
1652                            &phases_by_id,
1653                            &asserted_phase_ids,
1654                        ),
1655                    }
1656                })
1657                .collect::<Vec<_>>();
1658            BranchResult {
1659                covered: alternatives.iter().all(|alternative| alternative.covered),
1660                meta,
1661                alternatives,
1662            }
1663        })
1664        .collect::<Vec<_>>();
1665
1666    // Obligations the frontend declined to measure leave the covered/uncovered
1667    // denominator entirely. Counting them as uncovered would report a
1668    // measurement gap as a coverage gap — a wrong number, and wrong numbers get
1669    // trusted. They are reported separately instead, alongside the share of
1670    // obligations that were measured exactly.
1671    let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
1672
1673    // Lines are folded from every point, declined ones included, so a file
1674    // Supercov could not measure still has addressable lines to report a
1675    // limitation against. Only what was measured decides the line's state and
1676    // whether it counts.
1677    let mut line_aggregates = BTreeMap::<SourceLine, LineAggregate>::new();
1678    for point in &points {
1679        let aggregate = line_aggregates
1680            .entry(SourceLine {
1681                file: point.meta.file.clone(),
1682                line: point.meta.line,
1683            })
1684            .or_default();
1685        if declined.contains(&point.meta.id) {
1686            continue;
1687        }
1688        aggregate.measured = true;
1689        aggregate.covered |= point.covered;
1690        aggregate.tests.extend(point.tests.clone());
1691        aggregate.phases.extend(point.phases.clone());
1692        aggregate.explicit_phases.extend(
1693            explicit_phases_by_hit
1694                .get(&point.meta.id)
1695                .into_iter()
1696                .flatten()
1697                .cloned(),
1698        );
1699    }
1700    let lines = line_aggregates
1701        .into_iter()
1702        .map(|(location, aggregate)| {
1703            let LineAggregate {
1704                covered,
1705                measured,
1706                tests: test_ids,
1707                phases: phase_ids,
1708                explicit_phases: explicit_ids,
1709            } = aggregate;
1710            let provenances = test_ids
1711                .iter()
1712                .filter_map(|id| tests_by_id.get(id).map(|test| &test.provenance))
1713                .collect::<Vec<_>>();
1714            let runners = provenances
1715                .iter()
1716                .map(|provenance| provenance.runner.clone())
1717                .collect::<BTreeSet<_>>();
1718            let kinds = provenances
1719                .iter()
1720                .map(|provenance| provenance.kind.clone())
1721                .collect::<BTreeSet<_>>();
1722            LineResult {
1723                file: location.file,
1724                line: location.line,
1725                covered,
1726                measured,
1727                tests: sorted(&test_ids),
1728                runners: sorted(&runners),
1729                exclusive_kind: (kinds.len() == 1).then(|| kinds.first().unwrap().clone()),
1730                phases: sorted(&phase_ids),
1731                confidence: confidence_for(
1732                    sorted(&test_ids),
1733                    sorted(&phase_ids),
1734                    sorted(&explicit_ids),
1735                    &tests_by_id,
1736                    &phases_by_id,
1737                    &asserted_phase_ids,
1738                ),
1739                kinds: sorted(&kinds),
1740            }
1741        })
1742        .collect::<Vec<_>>();
1743
1744    let point_locations = manifest
1745        .points
1746        .iter()
1747        .map(|point| {
1748            (
1749                point.id.clone(),
1750                SourceLine {
1751                    file: point.file.clone(),
1752                    line: point.line,
1753                },
1754            )
1755        })
1756        .collect::<HashMap<_, _>>();
1757    test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
1758    let tests = test_order
1759        .into_iter()
1760        .map(|id| {
1761            let test = tests_by_id.get(&id).expect("test order references test");
1762            let lines = test
1763                .hits
1764                .iter()
1765                .filter_map(|hit| point_locations.get(hit).cloned())
1766                .collect::<BTreeSet<_>>();
1767            TestCoverageResult {
1768                id: test.id.clone(),
1769                name: test.name.clone(),
1770                file: test.file.clone(),
1771                title: test.title.clone(),
1772                retries: sorted(&test.retries),
1773                attempts: test.attempts.values().cloned().collect(),
1774                outcome: test_outcome(test),
1775                provenance: test.provenance.clone(),
1776                role: test.role.clone(),
1777                hits: sorted(&test.hits),
1778                decisions: test
1779                    .decisions
1780                    .iter()
1781                    .map(|(id, vectors)| TestDecisionResult {
1782                        id: id.clone(),
1783                        vectors: vectors.values.clone(),
1784                    })
1785                    .collect(),
1786                lines: lines.into_iter().collect(),
1787            }
1788        })
1789        .collect::<Vec<_>>();
1790
1791    let mut test_files = BTreeMap::<
1792        String,
1793        (
1794            BTreeSet<String>,
1795            BTreeSet<String>,
1796            BTreeSet<String>,
1797            BTreeSet<SourceLine>,
1798        ),
1799    >::new();
1800    for test in &tests {
1801        let aggregate = test_files
1802            .entry(
1803                test.file
1804                    .clone()
1805                    .unwrap_or_else(|| "(unknown test file)".into()),
1806            )
1807            .or_default();
1808        aggregate.0.insert(test.id.clone());
1809        aggregate.1.insert(test.provenance.runner.clone());
1810        aggregate.2.insert(test.provenance.kind.clone());
1811        aggregate.3.extend(test.lines.clone());
1812    }
1813    let test_files = test_files
1814        .into_iter()
1815        .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
1816            file,
1817            tests: sorted(&tests),
1818            runners: sorted(&runners),
1819            kinds: sorted(&kinds),
1820            lines: lines.into_iter().collect(),
1821        })
1822        .collect::<Vec<_>>();
1823
1824    let mut phases = phases_by_id.values().cloned().collect::<Vec<_>>();
1825    phases.sort_by(|left, right| {
1826        left.phase
1827            .started_at_ms
1828            .cmp(&right.phase.started_at_ms)
1829            .then(left.phase.id.cmp(&right.phase.id))
1830    });
1831    let phases = phases
1832        .into_iter()
1833        .map(|phase| {
1834            let lines = phase
1835                .hits
1836                .iter()
1837                .filter_map(|hit| point_locations.get(hit).cloned())
1838                .collect::<BTreeSet<_>>();
1839            PhaseResult {
1840                phase: phase.phase,
1841                test: phase.test,
1842                hits: sorted(&phase.hits),
1843                decisions: phase
1844                    .decisions
1845                    .into_iter()
1846                    .map(|(id, vectors)| TestDecisionResult {
1847                        id,
1848                        vectors: vectors.values,
1849                    })
1850                    .collect(),
1851                lines: lines.into_iter().collect(),
1852                browser_events: phase.browser_events,
1853                server_events: phase.server_events,
1854                explicit_events: phase.explicit_events,
1855                inferred_events: phase.inferred_events,
1856                explicit_browser_events: phase.explicit_browser_events,
1857                inferred_browser_events: phase.inferred_browser_events,
1858                explicit_server_events: phase.explicit_server_events,
1859                inferred_server_events: phase.inferred_server_events,
1860            }
1861        })
1862        .collect::<Vec<_>>();
1863
1864    let total_obligations = decisions.len() + points.len() + branches.len();
1865    let (decisions, points, branches) = if declined.is_empty() {
1866        (decisions, points, branches)
1867    } else {
1868        (
1869            decisions
1870                .into_iter()
1871                .filter(|result| !declined.contains(&result.meta.id))
1872                .collect::<Vec<_>>(),
1873            points
1874                .into_iter()
1875                .filter(|result| !declined.contains(&result.meta.id))
1876                .collect::<Vec<_>>(),
1877            branches
1878                .into_iter()
1879                .filter(|result| !declined.contains(&result.meta.id))
1880                .collect::<Vec<_>>(),
1881        )
1882    };
1883    let measured_obligations = decisions.len() + points.len() + branches.len();
1884    let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
1885    if total_obligations > measured_obligations {
1886        summary.unmeasured_obligations = Some(total_obligations - measured_obligations);
1887        summary.exact_fraction_pct = Some(if total_obligations == 0 {
1888            100.0
1889        } else {
1890            (measured_obligations as f64) * 100.0 / (total_obligations as f64)
1891        });
1892    }
1893    if !manifest.limitations.is_empty() {
1894        summary.coverage_complete = false;
1895        summary.completeness_blocked = Some(true);
1896    }
1897
1898    let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
1899        let values = tests
1900            .iter()
1901            .map(|test| {
1902                if field == "kind" {
1903                    test.provenance.kind.clone()
1904                } else {
1905                    test.provenance.runner.clone()
1906                }
1907            })
1908            .collect::<BTreeSet<_>>();
1909        values
1910            .into_iter()
1911            .map(|value| {
1912                let selected = tests
1913                    .iter()
1914                    .filter(|test| {
1915                        if field == "kind" {
1916                            test.provenance.kind == value
1917                        } else {
1918                            test.provenance.runner == value
1919                        }
1920                    })
1921                    .map(|test| test.id.clone())
1922                    .collect::<BTreeSet<_>>();
1923                Ok(DimensionCoverage {
1924                    kind: (field == "kind").then(|| value.clone()),
1925                    runner: (field == "runner").then(|| value.clone()),
1926                    tests: tests
1927                        .iter()
1928                        .filter(|test| selected.contains(&test.id) && test.role == "test")
1929                        .count(),
1930                    setups: tests
1931                        .iter()
1932                        .filter(|test| selected.contains(&test.id) && test.role == "setup")
1933                        .count(),
1934                    summary: summary_for_results(
1935                        &decisions,
1936                        &points,
1937                        &branches,
1938                        &lines,
1939                        Some(&selected),
1940                    )?,
1941                })
1942            })
1943            .collect()
1944    };
1945
1946    Ok(CoverageView {
1947        generated_at: generated_at.into(),
1948        variant: coverage_model.variant.clone(),
1949        scope: manifest.scope.clone(),
1950        model: CoverageModel {
1951            language: coverage_model.language.clone(),
1952            name: coverage_model.name.clone(),
1953            completeness_meaning: coverage_model.completeness_meaning.clone(),
1954            measured: coverage_model.measured.clone(),
1955            not_measured: coverage_model.not_measured.clone(),
1956        },
1957        integrity: None,
1958        limitations: manifest.limitations.clone(),
1959        transport: None,
1960        summary,
1961        coverage_by_kind: dimension_coverage("kind")?,
1962        coverage_by_runner: dimension_coverage("runner")?,
1963        decisions,
1964        points,
1965        branches,
1966        tests,
1967        test_files,
1968        phases,
1969        lines,
1970    })
1971}
1972
1973pub fn analyze_coverage_results(
1974    request: &CoverageReportRequest,
1975) -> Result<CoverageReport, ReportError> {
1976    if let Some(scope) = request
1977        .raw_results
1978        .iter()
1979        .filter_map(|raw| raw.scope.as_ref())
1980        .find(|scope| scope.run_id != request.run_id)
1981    {
1982        return Err(ReportError::ScopeMismatch {
1983            expected: request.run_id.clone(),
1984            actual: scope.run_id.clone(),
1985        });
1986    }
1987    if request.raw_results.is_empty() {
1988        return Err(ReportError::NoEvidence(request.run_id.clone()));
1989    }
1990    let default_model = javascript_coverage_model();
1991    let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
1992    PersistedCoverageModel::from_declaration(coverage_model).map_err(|reason| {
1993        ReportError::InvalidJson {
1994            path: "coverage-model.json".into(),
1995            reason: reason.into(),
1996        }
1997    })?;
1998    let view = create_coverage_view_with_model(
1999        &request.manifest,
2000        &request.raw_results,
2001        &request.generated_at,
2002        coverage_model,
2003    )?;
2004    let passed = create_coverage_view_with_model(
2005        &request.manifest,
2006        &passing_coverage_results(&request.raw_results),
2007        &request.generated_at,
2008        coverage_model,
2009    )?;
2010    let failed = create_coverage_view_with_model(
2011        &request.manifest,
2012        &failed_coverage_results(&request.raw_results),
2013        &request.generated_at,
2014        coverage_model,
2015    )?;
2016    let execution = match request.test_exit_code {
2017        ExitCodeInput::Missing => None,
2018        ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
2019            valid: test_exit_code == Some(0),
2020            test_exit_code,
2021        }),
2022    };
2023    let mut view = view;
2024    let mut passed = passed;
2025    let mut failed = failed;
2026    if let Some(integrity) = &request.integrity {
2027        view.integrity = Some(integrity.clone());
2028        passed.integrity = Some(integrity.clone());
2029        failed.integrity = Some(integrity.clone());
2030    }
2031    Ok(CoverageReport {
2032        view,
2033        execution,
2034        filters: CoverageFilters { passed, failed },
2035    })
2036}
2037
2038fn parse_entry<T: for<'de> Deserialize<'de>>(
2039    entry: &EvidenceArchiveEntry,
2040) -> Result<T, ReportError> {
2041    serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
2042        path: entry.path.clone(),
2043        reason: error.to_string(),
2044    })
2045}
2046
2047fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
2048    entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2049) -> Result<Vec<T>, ReportError> {
2050    let mut records = Vec::new();
2051    for entry in entries {
2052        let Some(contents) = entry.contents.strip_suffix(b"\n") else {
2053            return Err(ReportError::InvalidJson {
2054                path: entry.path.clone(),
2055                reason: "recognized JSONL evidence must end with a newline".into(),
2056            });
2057        };
2058        if contents.is_empty() {
2059            return Err(ReportError::InvalidJson {
2060                path: entry.path.clone(),
2061                reason: "recognized JSONL evidence must contain at least one record".into(),
2062            });
2063        }
2064        for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
2065            if line.is_empty() {
2066                return Err(ReportError::InvalidJson {
2067                    path: entry.path.clone(),
2068                    reason: format!("blank JSONL record at line {}", index + 1),
2069                });
2070            }
2071            records.push(serde_json::from_slice(line).map_err(|error| {
2072                ReportError::InvalidJson {
2073                    path: entry.path.clone(),
2074                    reason: format!("invalid JSONL record at line {}: {error}", index + 1),
2075                }
2076            })?);
2077        }
2078    }
2079    Ok(records)
2080}
2081
2082/// Server evidence is appended by application processes Supercov does not
2083/// control — including pool VMs restored from one snapshot, whose clones can
2084/// tear a shared shard. A torn line is one lost record, not a lost run: it is
2085/// skipped and counted, and the report says so through the existing
2086/// CORRUPT_EVIDENCE_RECORDS diagnostic and the blocking-limitation total.
2087struct TolerantJsonLines<T> {
2088    records: Vec<T>,
2089    corrupt_records: usize,
2090    corrupt_files: usize,
2091}
2092
2093fn parse_server_json_lines<'a, T: for<'de> Deserialize<'de>>(
2094    entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
2095) -> TolerantJsonLines<T> {
2096    let mut parsed = TolerantJsonLines {
2097        records: Vec::new(),
2098        corrupt_records: 0,
2099        corrupt_files: 0,
2100    };
2101    for entry in entries {
2102        let mut corrupt_here = 0;
2103        for line in entry.contents.split(|byte| *byte == b'\n') {
2104            if line.is_empty() {
2105                continue;
2106            }
2107            match serde_json::from_slice(line) {
2108                Ok(record) => parsed.records.push(record),
2109                Err(_) => corrupt_here += 1,
2110            }
2111        }
2112        if corrupt_here > 0 {
2113            parsed.corrupt_records += corrupt_here;
2114            parsed.corrupt_files += 1;
2115        }
2116    }
2117    parsed
2118}
2119
2120fn is_mcdc_result(path: &str) -> bool {
2121    path == "mcdc.json" || path.ends_with("/mcdc.json")
2122}
2123
2124fn is_mcdc_journal(path: &str) -> bool {
2125    path == "mcdc.jsonl" || path.ends_with(".mcdc.jsonl")
2126}
2127
2128fn validate_rust_compiler_scope(manifest: &CoverageManifest) -> Result<(), ReportError> {
2129    let scope = manifest
2130        .scope
2131        .as_ref()
2132        .and_then(Value::as_object)
2133        .ok_or_else(|| ReportError::InvalidArchive("missing Rust compiler source scope".into()))?;
2134    let expected = BTreeSet::from([
2135        "crate",
2136        "language",
2137        "measurementComplete",
2138        "model",
2139        "sourceFingerprint",
2140    ]);
2141    if scope.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected
2142        || scope.get("language").and_then(Value::as_str) != Some("rust")
2143        || scope.get("model").and_then(Value::as_str) != Some("rust-source-v1")
2144        || scope
2145            .get("crate")
2146            .and_then(Value::as_str)
2147            .is_none_or(str::is_empty)
2148        || !scope
2149            .get("measurementComplete")
2150            .is_some_and(Value::is_boolean)
2151    {
2152        return Err(ReportError::InvalidArchive(
2153            "malformed Rust compiler source scope".into(),
2154        ));
2155    }
2156    let fingerprint = scope
2157        .get("sourceFingerprint")
2158        .and_then(Value::as_object)
2159        .ok_or_else(|| {
2160            ReportError::InvalidArchive("missing Rust compiler source fingerprint".into())
2161        })?;
2162    let expected_fingerprint = BTreeSet::from(["algorithm", "digest", "files", "generatedFiles"]);
2163    let digest = fingerprint.get("digest").and_then(Value::as_str);
2164    let files = fingerprint.get("files").and_then(Value::as_u64);
2165    let generated = fingerprint.get("generatedFiles").and_then(Value::as_u64);
2166    if fingerprint
2167        .keys()
2168        .map(String::as_str)
2169        .collect::<BTreeSet<_>>()
2170        != expected_fingerprint
2171        || fingerprint.get("algorithm").and_then(Value::as_str) != Some("sha256")
2172        || !digest.is_some_and(|digest| {
2173            digest.len() == 64
2174                && digest
2175                    .bytes()
2176                    .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2177        })
2178        || files.is_none_or(|files| files == 0)
2179        || generated
2180            .zip(files)
2181            .is_none_or(|(generated, files)| generated > files)
2182    {
2183        return Err(ReportError::InvalidArchive(
2184            "malformed Rust compiler source fingerprint".into(),
2185        ));
2186    }
2187    Ok(())
2188}
2189
2190pub fn analyze_coverage_archive(
2191    request: &ArchiveReportRequest,
2192) -> Result<CoverageReport, ReportError> {
2193    let entries = read_archive(Path::new(&request.archive_path))
2194        .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2195    let manifest = entries
2196        .iter()
2197        .find(|entry| entry.path == "manifest.json")
2198        .ok_or(ReportError::MissingManifest)
2199        .and_then(parse_entry::<CoverageManifest>)?;
2200    let mut raw_results = entries
2201        .iter()
2202        .filter(|entry| is_mcdc_result(&entry.path))
2203        .map(parse_entry::<RawTestResult>)
2204        .collect::<Result<Vec<_>, _>>()?;
2205    let journal_results = parse_json_lines::<RawTestResult>(
2206        entries.iter().filter(|entry| is_mcdc_journal(&entry.path)),
2207    )?;
2208    raw_results.extend(journal_results);
2209
2210    let scoped = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2211        entry.path.starts_with("server/")
2212            && !entry.path.starts_with("server/background/")
2213            && entry.path.ends_with(".jsonl")
2214    }));
2215    let scoped_records = scoped.records;
2216    for record in &scoped_records {
2217        let Some(scope) = &record.scope else { continue };
2218        let Some(raw) = raw_results
2219            .iter_mut()
2220            .find(|raw| raw.scope.as_ref() == Some(scope))
2221        else {
2222            continue;
2223        };
2224        if !raw.server.contains(record) {
2225            raw.server.push(record.clone());
2226        }
2227    }
2228
2229    let background = parse_server_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
2230        entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
2231    }));
2232    let background_records = background.records;
2233    if !background_records.is_empty() {
2234        raw_results.push(RawTestResult {
2235            test_id: Some(format!("background:{}", request.run_id)),
2236            scope: None,
2237            test: "Background / unattributed".into(),
2238            test_file: None,
2239            title: Some("Background / unattributed".into()),
2240            retry: None,
2241            status: Some("unknown".into()),
2242            expected_status: None,
2243            flaky: false,
2244            provenance: TestProvenance {
2245                runner: "background".into(),
2246                kind: "background".into(),
2247                project: None,
2248                source: "explicit".into(),
2249            },
2250            role: "background".into(),
2251            phases: vec![],
2252            runtime: vec![],
2253            browser: vec![],
2254            server: background_records.clone(),
2255        });
2256    }
2257
2258    // Execution traces are appended by the launch observer inside application
2259    // processes, so they share the clone hazard of server evidence.
2260    let execution =
2261        parse_server_json_lines::<ExecutionTraceEvent>(entries.iter().filter(|entry| {
2262            entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
2263        }));
2264    let execution_events = execution.records;
2265    let count_event = |name: &str| {
2266        execution_events
2267            .iter()
2268            .filter(|event| event.kind() == name)
2269            .count()
2270    };
2271    let transport = TransportStats {
2272        processes: count_event("process"),
2273        child_launches: count_event("child-launch"),
2274        remote_launches: count_event("remote-launch"),
2275        workspace_capabilities: count_event("workspace-capability"),
2276        scoped_server_records: scoped_records.len(),
2277        background_server_records: background_records.len(),
2278        corrupt_records: scoped.corrupt_records
2279            + background.corrupt_records
2280            + execution.corrupt_records,
2281        corrupt_files: scoped.corrupt_files + background.corrupt_files + execution.corrupt_files,
2282    };
2283    let frontend = entries
2284        .iter()
2285        .find(|entry| entry.path == "frontend.json")
2286        .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
2287        .and_then(parse_entry::<FrontendRunDeclaration>)?;
2288    let persisted = entries
2289        .iter()
2290        .find(|entry| entry.path == "coverage-model.json")
2291        .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
2292        .and_then(parse_entry::<PersistedCoverageModel>)?;
2293    let coverage_model =
2294        persisted
2295            .into_declaration()
2296            .map_err(|reason| ReportError::InvalidJson {
2297                path: "coverage-model.json".into(),
2298                reason: reason.into(),
2299            })?;
2300    if frontend.language != coverage_model.language {
2301        return Err(ReportError::InvalidArchive(format!(
2302            "frontend language {} differs from coverage model language {}",
2303            frontend.language, coverage_model.language
2304        )));
2305    }
2306    if frontend.frontend_version == "rust-compiler-v1" {
2307        validate_rust_compiler_scope(&manifest)?;
2308    }
2309    let normalized = CoverageReportRequest {
2310        run_id: request.run_id.clone(),
2311        manifest,
2312        raw_results,
2313        generated_at: request.generated_at.clone(),
2314        coverage_model: Some(coverage_model),
2315        integrity: request.integrity.clone(),
2316        test_exit_code: request.test_exit_code.clone(),
2317    };
2318    let mut report = crate::frontend_protocol::analyze_frontend_results(&frontend, &normalized)
2319        .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
2320    report.view.transport = Some(transport.clone());
2321    report.filters.passed.transport = Some(transport.clone());
2322    report.filters.failed.transport = Some(transport);
2323    Ok(report)
2324}
2325
2326#[cfg(test)]
2327mod tests {
2328    use std::{
2329        fs,
2330        sync::atomic::{AtomicU64, Ordering},
2331        time::SystemTime,
2332    };
2333
2334    use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
2335
2336    use super::*;
2337
2338    static ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
2339
2340    fn point(id: &str, line: usize) -> PointMeta {
2341        PointMeta {
2342            id: id.into(),
2343            kind: PointKind::Statement,
2344            file: "src/app.js".into(),
2345            line,
2346            column: 0,
2347            source: "work();".into(),
2348            label: None,
2349        }
2350    }
2351
2352    fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
2353        RawTestResult {
2354            test_id: Some(id.into()),
2355            scope: None,
2356            test: id.into(),
2357            test_file: Some("tests/app.test.js".into()),
2358            title: None,
2359            retry: Some(retry),
2360            status: Some(status.into()),
2361            expected_status: None,
2362            flaky: false,
2363            provenance: TestProvenance {
2364                runner: "node:test".into(),
2365                kind: "unit".into(),
2366                project: None,
2367                source: "runner-default".into(),
2368            },
2369            role: "test".into(),
2370            phases: vec![],
2371            runtime: vec![RuntimeSnapshot {
2372                decisions: vec![],
2373                hits: hits.iter().map(|hit| (*hit).into()).collect(),
2374                events: vec![],
2375            }],
2376            browser: vec![],
2377            server: vec![],
2378        }
2379    }
2380
2381    #[test]
2382    fn declined_obligations_are_unmeasured_never_uncovered() {
2383        // Two statements, neither executed. One of them Supercov declined to
2384        // measure. The declined one must not appear as a coverage gap: it
2385        // leaves the denominator and is reported as unmeasured instead.
2386        let point = |id: &str, line: usize| PointMeta {
2387            id: id.into(),
2388            kind: PointKind::Statement,
2389            file: "src/app.js".into(),
2390            line,
2391            column: 0,
2392            source: "work();".into(),
2393            label: None,
2394        };
2395        let measured_only = CoverageManifest {
2396            decisions: vec![],
2397            points: vec![point("measured", 1)],
2398            branches: vec![],
2399            limitations: vec![],
2400            unmeasured: Vec::new(),
2401            scope: None,
2402        };
2403        let with_declined = CoverageManifest {
2404            decisions: vec![],
2405            points: vec![point("measured", 1), point("declined", 2)],
2406            branches: vec![],
2407            limitations: vec![],
2408            unmeasured: vec!["declined".into()],
2409            scope: None,
2410        };
2411
2412        let baseline =
2413            create_coverage_view(&measured_only, &[raw("test", 0, "passed", &[])], "time").unwrap();
2414        let view =
2415            create_coverage_view(&with_declined, &[raw("test", 0, "passed", &[])], "time").unwrap();
2416
2417        // The declined statement never inflates the uncovered count.
2418        assert_eq!(
2419            view.summary.statements.total, baseline.summary.statements.total,
2420            "a declined obligation stayed in the covered/uncovered denominator"
2421        );
2422        assert_eq!(view.summary.unmeasured_obligations, Some(1));
2423        assert_eq!(view.summary.exact_fraction_pct, Some(50.0));
2424
2425        // Its line leaves the line total too, but not the report: the line is
2426        // still there to hang a limitation on, and it is neither covered nor
2427        // uncovered.
2428        assert_eq!(
2429            view.summary.lines.total, baseline.summary.lines.total,
2430            "a declined obligation kept its line in the line denominator"
2431        );
2432        let declined_line = view
2433            .lines
2434            .iter()
2435            .find(|line| line.line == 2)
2436            .expect("the declined line stays addressable");
2437        assert!(!declined_line.measured);
2438        assert!(!declined_line.covered);
2439
2440        // A fully measured run keeps its previous output exactly: the new
2441        // fields are absent, not zero, so existing consumers see no change.
2442        assert_eq!(baseline.summary.unmeasured_obligations, None);
2443        assert_eq!(baseline.summary.exact_fraction_pct, None);
2444        let encoded = serde_json::to_string(&baseline.summary).unwrap();
2445        assert!(
2446            !encoded.contains("unmeasured") && !encoded.contains("exactFraction"),
2447            "a fully measured summary must serialize unchanged: {encoded}"
2448        );
2449    }
2450
2451    #[test]
2452    fn a_declined_line_that_executed_is_neither_covered_nor_uncovered() {
2453        // The subtle case: the statement ran, but Supercov could not measure
2454        // it, so its line has no coverage question to answer. Counting it as
2455        // covered would inflate the ratio with a line nothing verified.
2456        let point = |id: &str| PointMeta {
2457            id: id.into(),
2458            kind: PointKind::Statement,
2459            file: "src/app.js".into(),
2460            line: 1,
2461            column: 0,
2462            source: "work();".into(),
2463            label: None,
2464        };
2465        let manifest = CoverageManifest {
2466            decisions: vec![],
2467            points: vec![point("declined")],
2468            branches: vec![],
2469            limitations: vec![],
2470            unmeasured: vec!["declined".into()],
2471            scope: None,
2472        };
2473        let view = create_coverage_view(
2474            &manifest,
2475            &[raw("test", 0, "passed", &["declined"])],
2476            "time",
2477        )
2478        .unwrap();
2479
2480        assert_eq!(view.summary.lines.total, 0);
2481        assert_eq!(view.summary.lines.covered, 0);
2482        let line = view.lines.first().expect("the line stays addressable");
2483        assert!(!line.measured);
2484        assert!(!line.covered, "an unmeasured line is not covered either");
2485    }
2486
2487    #[test]
2488    fn report_retains_unexecuted_manifest_conditions() {
2489        let manifest = CoverageManifest {
2490            unmeasured: Vec::new(),
2491            decisions: vec![DecisionMeta {
2492                id: "decision".into(),
2493                file: "src/app.js".into(),
2494                line: 1,
2495                column: 0,
2496                source: "left && right".into(),
2497                conditions: vec!["left".into(), "right".into()],
2498                kind: "if".into(),
2499            }],
2500            points: vec![],
2501            branches: vec![],
2502            limitations: vec![],
2503            scope: None,
2504        };
2505        let view =
2506            create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
2507        assert_eq!(view.summary.conditions, 2);
2508        assert_eq!(view.summary.covered_conditions, 0);
2509        assert_eq!(view.decisions[0].conditions.len(), 2);
2510    }
2511
2512    #[test]
2513    fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
2514        let manifest = CoverageManifest {
2515            unmeasured: Vec::new(),
2516            decisions: vec![DecisionMeta {
2517                id: "application-decision".into(),
2518                file: "src/app.js".into(),
2519                line: 1,
2520                column: 0,
2521                source: "left && right".into(),
2522                conditions: vec!["left".into(), "right".into()],
2523                kind: "if".into(),
2524            }],
2525            points: vec![],
2526            branches: vec![],
2527            limitations: vec![],
2528            scope: None,
2529        };
2530        let mut attempt = raw("test", 0, "passed", &[]);
2531        attempt.runtime[0].decisions.push(DecisionSnapshot {
2532            meta: DecisionMeta {
2533                id: "synthetic-fixture".into(),
2534                file: "fixtures/generated.js".into(),
2535                line: 1,
2536                column: 0,
2537                source: "a && b && c".into(),
2538                conditions: vec!["a".into(), "b".into(), "c".into()],
2539                kind: "if".into(),
2540            },
2541            vectors: vec![McdcVector {
2542                values: vec![Some(true), Some(true), Some(true)],
2543                outcome: true,
2544            }],
2545        });
2546
2547        let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2548        assert_eq!(view.decisions.len(), 1);
2549        assert_eq!(view.decisions[0].meta.id, "application-decision");
2550        assert!(view.decisions[0].vectors.is_empty());
2551    }
2552
2553    #[test]
2554    fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
2555        let expected = DecisionMeta {
2556            id: "application-decision".into(),
2557            file: "src/app.js".into(),
2558            line: 1,
2559            column: 0,
2560            source: "left && right".into(),
2561            conditions: vec!["left".into(), "right".into()],
2562            kind: "if".into(),
2563        };
2564        let manifest = CoverageManifest {
2565            unmeasured: Vec::new(),
2566            decisions: vec![expected.clone()],
2567            points: vec![],
2568            branches: vec![],
2569            limitations: vec![],
2570            scope: Some(serde_json::json!({
2571                "entries": [{ "file": "src/empty.js", "status": "included" }]
2572            })),
2573        };
2574        let vector = McdcVector {
2575            values: vec![Some(true), Some(true)],
2576            outcome: true,
2577        };
2578
2579        let mut unknown = raw("test", 0, "passed", &[]);
2580        unknown.runtime[0].decisions.push(DecisionSnapshot {
2581            meta: DecisionMeta {
2582                id: "unknown-decision".into(),
2583                file: "src/empty.js".into(),
2584                ..expected.clone()
2585            },
2586            vectors: vec![vector.clone()],
2587        });
2588        assert!(matches!(
2589            create_coverage_view(&manifest, &[unknown], "time"),
2590            Err(ReportError::InvalidServerRecord(reason))
2591                if reason.contains("absent from the frozen manifest")
2592        ));
2593
2594        let mut changed = raw("test", 0, "passed", &[]);
2595        changed.runtime[0].decisions.push(DecisionSnapshot {
2596            meta: DecisionMeta {
2597                source: "left || right".into(),
2598                ..expected
2599            },
2600            vectors: vec![vector],
2601        });
2602        assert!(matches!(
2603            create_coverage_view(&manifest, &[changed], "time"),
2604            Err(ReportError::InvalidServerRecord(reason))
2605                if reason.contains("differs from the frozen manifest")
2606        ));
2607    }
2608
2609    #[test]
2610    fn timestamp_overlap_cannot_upgrade_assertion_confidence() {
2611        let manifest = CoverageManifest {
2612            unmeasured: Vec::new(),
2613            decisions: vec![],
2614            points: vec![point("hit", 1)],
2615            branches: vec![],
2616            limitations: vec![],
2617            scope: None,
2618        };
2619        let phase = CoveragePhase {
2620            id: "assertion".into(),
2621            kind: "assertion".into(),
2622            operation: "equal".into(),
2623            source: None,
2624            caused_by_phase_id: None,
2625            started_at_ms: 100,
2626            ended_at_ms: Some(120),
2627            status: Some("passed".into()),
2628            error: None,
2629        };
2630        let mut attempt = raw("test", 0, "passed", &["hit"]);
2631        attempt.phases.push(phase.clone());
2632        attempt.runtime[0].events.push(RuntimeEvent {
2633            event_type: "hit".into(),
2634            id: "hit".into(),
2635            vector: None,
2636            timestamp_ms: 110,
2637            phase_id: None,
2638            environment: "server".into(),
2639        });
2640        let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
2641        assert_eq!(inferred.points[0].confidence.level, "executed");
2642        assert!(!inferred.points[0].confidence.asserted);
2643
2644        attempt.runtime[0].events[0].phase_id = Some(phase.id);
2645        let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2646        assert_eq!(explicit.points[0].confidence.level, "asserted");
2647        assert!(explicit.points[0].confidence.asserted);
2648    }
2649
2650    #[test]
2651    fn verified_view_uses_only_the_terminal_successful_attempt() {
2652        let manifest = CoverageManifest {
2653            unmeasured: Vec::new(),
2654            decisions: vec![],
2655            points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
2656            branches: vec![],
2657            limitations: vec![],
2658            scope: None,
2659        };
2660        let failed = raw("flaky", 0, "failed", &["failed"]);
2661        let mut passed = raw("flaky", 1, "passed", &["passed"]);
2662        passed.flaky = true;
2663        let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
2664        expected.expected_status = Some("failed".into());
2665        let request = CoverageReportRequest {
2666            run_id: "run".into(),
2667            manifest,
2668            raw_results: vec![failed, passed, expected],
2669            generated_at: "time".into(),
2670            coverage_model: None,
2671            integrity: None,
2672            test_exit_code: ExitCodeInput::Missing,
2673        };
2674        let report = analyze_coverage_results(&request).unwrap();
2675        assert!(!report.filters.passed.points[0].covered);
2676        assert!(report.filters.passed.points[1].covered);
2677        assert!(!report.filters.passed.points[2].covered);
2678        assert!(report.filters.failed.points[0].covered);
2679        assert_eq!(
2680            report
2681                .view
2682                .tests
2683                .iter()
2684                .find(|test| test.name == "expected-failure")
2685                .unwrap()
2686                .outcome,
2687            "failed"
2688        );
2689        assert_eq!(report.view.tests[1].outcome, "flaky");
2690    }
2691
2692    #[test]
2693    fn expected_failure_is_a_green_outcome_but_not_verified_or_failed_coverage() {
2694        let mut expected = raw("expected-failure", 0, "failed", &["expected"]);
2695        expected.expected_status = Some("failed".into());
2696        let companion = raw("expected-failure", 0, "failed", &["expected"]);
2697        let request = CoverageReportRequest {
2698            run_id: "run".into(),
2699            manifest: CoverageManifest {
2700                unmeasured: Vec::new(),
2701                decisions: vec![],
2702                points: vec![point("expected", 1)],
2703                branches: vec![],
2704                limitations: vec![],
2705                scope: None,
2706            },
2707            raw_results: vec![companion, expected],
2708            generated_at: "time".into(),
2709            coverage_model: None,
2710            integrity: None,
2711            test_exit_code: ExitCodeInput::Missing,
2712        };
2713        let report = analyze_coverage_results(&request).unwrap();
2714        assert_eq!(report.view.tests[0].outcome, "passed");
2715        assert!(!report.filters.passed.points[0].covered);
2716        assert!(!report.filters.failed.points[0].covered);
2717    }
2718
2719    #[test]
2720    fn selected_but_unstarted_test_is_not_an_invented_attempt() {
2721        let mut unstarted = raw("unstarted", 0, "unstarted", &[]);
2722        unstarted.scope = None;
2723        unstarted.retry = None;
2724        unstarted.runtime.clear();
2725        let request = CoverageReportRequest {
2726            run_id: "run".into(),
2727            manifest: CoverageManifest {
2728                unmeasured: Vec::new(),
2729                decisions: vec![],
2730                points: vec![],
2731                branches: vec![],
2732                limitations: vec![],
2733                scope: None,
2734            },
2735            raw_results: vec![unstarted],
2736            generated_at: "time".into(),
2737            coverage_model: None,
2738            integrity: None,
2739            test_exit_code: ExitCodeInput::Present(Some(100)),
2740        };
2741        let report = analyze_coverage_results(&request).unwrap();
2742        assert_eq!(report.view.tests[0].outcome, "unstarted");
2743        assert!(report.view.tests[0].attempts.is_empty());
2744        assert!(report.filters.passed.tests.is_empty());
2745        assert!(report.filters.failed.tests.is_empty());
2746    }
2747
2748    fn archive(mut entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
2749        let nonce = SystemTime::now()
2750            .duration_since(SystemTime::UNIX_EPOCH)
2751            .unwrap()
2752            .as_nanos();
2753        let root = std::env::temp_dir().join(format!(
2754            "supercov-rust-report-{}-{nonce}-{}",
2755            std::process::id(),
2756            ARCHIVE_ID.fetch_add(1, Ordering::Relaxed)
2757        ));
2758        fs::create_dir_all(&root).unwrap();
2759        let path = root.join("evidence.raw.gz");
2760        if !entries
2761            .iter()
2762            .any(|entry| entry.path == "coverage-model.json")
2763        {
2764            entries.push(EvidenceArchiveEntry {
2765                path: "coverage-model.json".into(),
2766                contents: serde_json::to_vec(
2767                    &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
2768                        .unwrap(),
2769                )
2770                .unwrap(),
2771            });
2772        }
2773        if !entries.iter().any(|entry| entry.path == "frontend.json") {
2774            entries.push(EvidenceArchiveEntry {
2775                path: "frontend.json".into(),
2776                contents: serde_json::to_vec(&serde_json::json!({
2777                    "protocolVersion": 2,
2778                    "frontendId": "javascript",
2779                    "frontendVersion": "fixture-v1",
2780                    "language": "javascript",
2781                    "structuralSource": "owned-probes",
2782                    "runners": [{
2783                        "runner": "node:test",
2784                        "executionModel": "serial-in-process",
2785                        "attribution": {
2786                            "run": "exact",
2787                            "worker": "unavailable",
2788                            "test": "exact",
2789                            "retry": "exact",
2790                            "phase": "exact",
2791                            "action": "exact",
2792                            "assertion": "exact"
2793                        },
2794                        "limitations": [{
2795                            "id": "fixture-worker-unavailable",
2796                            "scopes": ["worker"],
2797                            "reason": "The fixture does not require worker identity"
2798                        }]
2799                    }],
2800                    "structuralLimitations": []
2801                }))
2802                .unwrap(),
2803            });
2804        }
2805        write_archive(entries, &path).unwrap();
2806        path
2807    }
2808
2809    #[test]
2810    fn archive_analysis_rejects_any_malformed_recognized_jsonl() {
2811        let manifest = CoverageManifest {
2812            unmeasured: Vec::new(),
2813            decisions: vec![],
2814            points: vec![point("background-hit", 1), point("test-hit", 2)],
2815            branches: vec![],
2816            limitations: vec![],
2817            scope: None,
2818        };
2819        // Declare exactly the observed runners: the journal's node:test and the
2820        // synthesized background runner, shaped as the JavaScript run declares it.
2821        let unattributed = |axis: &str| {
2822            serde_json::json!({
2823                "id": format!("background-no-{axis}"),
2824                "scopes": [axis],
2825                "reason": format!("Runner background did not expose exact {axis} identity for every result")
2826            })
2827        };
2828        let frontend = serde_json::json!({
2829            "protocolVersion": 2,
2830            "frontendId": "javascript",
2831            "frontendVersion": "fixture-v1",
2832            "language": "javascript",
2833            "structuralSource": "owned-probes",
2834            "runners": [
2835                {
2836                    "runner": "node:test",
2837                    "executionModel": "serial-in-process",
2838                    "attribution": {
2839                        "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2840                        "phase": "exact", "action": "exact", "assertion": "exact"
2841                    },
2842                    "limitations": [{
2843                        "id": "fixture-worker-unavailable",
2844                        "scopes": ["worker"],
2845                        "reason": "The fixture does not require worker identity"
2846                    }]
2847                },
2848                {
2849                    "runner": "background",
2850                    "executionModel": "parallel-unattributed",
2851                    "attribution": {
2852                        "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2853                        "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2854                    },
2855                    "limitations": [
2856                        unattributed("worker"), unattributed("test"), unattributed("retry"),
2857                        unattributed("phase"), unattributed("action"), unattributed("assertion")
2858                    ]
2859                }
2860            ],
2861            "structuralLimitations": []
2862        });
2863        let path = archive(vec![
2864            EvidenceArchiveEntry {
2865                path: "manifest.json".into(),
2866                contents: serde_json::to_vec(&manifest).unwrap(),
2867            },
2868            EvidenceArchiveEntry {
2869                path: "frontend.json".into(),
2870                contents: serde_json::to_vec(&frontend).unwrap(),
2871            },
2872            EvidenceArchiveEntry {
2873                path: "playwright-worker-1.mcdc.jsonl".into(),
2874                contents: {
2875                    let mut contents =
2876                        serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2877                            .unwrap();
2878                    contents.extend_from_slice(b"\npartial-final-line");
2879                    contents
2880                },
2881            },
2882            EvidenceArchiveEntry {
2883                path: "server/background/worker.jsonl".into(),
2884                contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
2885            },
2886        ]);
2887        let result = analyze_coverage_archive(&ArchiveReportRequest {
2888            archive_path: path.clone(),
2889            run_id: "run".into(),
2890            generated_at: "time".into(),
2891            integrity: None,
2892            test_exit_code: ExitCodeInput::Missing,
2893        });
2894        assert!(matches!(
2895            result,
2896            Err(ReportError::InvalidJson { path, .. })
2897                if path == "playwright-worker-1.mcdc.jsonl"
2898        ));
2899        fs::remove_dir_all(path.parent().unwrap()).unwrap();
2900    }
2901
2902    #[test]
2903    fn a_torn_background_record_is_counted_not_fatal() {
2904        // Pool VMs restored from one snapshot are clones with the same pid and
2905        // cached shard path; their appends over a shared mount tear lines. One
2906        // torn line is one lost record: the report must still build, count it,
2907        // and surface it through the corrupt-evidence accounting.
2908        let manifest = CoverageManifest {
2909            unmeasured: Vec::new(),
2910            decisions: vec![],
2911            points: vec![],
2912            branches: vec![],
2913            limitations: vec![],
2914            scope: None,
2915        };
2916        // Declare exactly the observed runners: the journal's node:test and the
2917        // synthesized background runner, shaped as the JavaScript run declares it.
2918        let unattributed = |axis: &str| {
2919            serde_json::json!({
2920                "id": format!("background-no-{axis}"),
2921                "scopes": [axis],
2922                "reason": format!("Runner background did not expose exact {axis} identity for every result")
2923            })
2924        };
2925        let frontend = serde_json::json!({
2926            "protocolVersion": 2,
2927            "frontendId": "javascript",
2928            "frontendVersion": "fixture-v1",
2929            "language": "javascript",
2930            "structuralSource": "owned-probes",
2931            "runners": [
2932                {
2933                    "runner": "node:test",
2934                    "executionModel": "serial-in-process",
2935                    "attribution": {
2936                        "run": "exact", "worker": "unavailable", "test": "exact", "retry": "exact",
2937                        "phase": "exact", "action": "exact", "assertion": "exact"
2938                    },
2939                    "limitations": [{
2940                        "id": "fixture-worker-unavailable",
2941                        "scopes": ["worker"],
2942                        "reason": "The fixture does not require worker identity"
2943                    }]
2944                },
2945                {
2946                    "runner": "background",
2947                    "executionModel": "parallel-unattributed",
2948                    "attribution": {
2949                        "run": "exact", "worker": "unavailable", "test": "unavailable", "retry": "unavailable",
2950                        "phase": "unavailable", "action": "unavailable", "assertion": "unavailable"
2951                    },
2952                    "limitations": [
2953                        unattributed("worker"), unattributed("test"), unattributed("retry"),
2954                        unattributed("phase"), unattributed("action"), unattributed("assertion")
2955                    ]
2956                }
2957            ],
2958            "structuralLimitations": []
2959        });
2960        let path = archive(vec![
2961            EvidenceArchiveEntry {
2962                path: "manifest.json".into(),
2963                contents: serde_json::to_vec(&manifest).unwrap(),
2964            },
2965            EvidenceArchiveEntry {
2966                path: "frontend.json".into(),
2967                contents: serde_json::to_vec(&frontend).unwrap(),
2968            },
2969            EvidenceArchiveEntry {
2970                path: "playwright-worker-1.mcdc.jsonl".into(),
2971                contents: {
2972                    let mut contents =
2973                        serde_json::to_vec(&raw("journal-test", 0, "passed", &["test-hit"]))
2974                            .unwrap();
2975                    contents.push(b'\n');
2976                    contents
2977                },
2978            },
2979            EvidenceArchiveEntry {
2980                path: "execution.37360-1.168.jsonl".into(),
2981                contents: b"418836ceb3ac9b6277f\"}}\n".to_vec(),
2982            },
2983            EvidenceArchiveEntry {
2984                path: "server/background/process-5118-a1b2c3d4-0.jsonl".into(),
2985                contents: concat!(
2986                    "{\"type\":\"hit\",\"id\":\"background-hit\"}\n",
2987                    "{\"type\":\"decision\",\"meta\":{\"id\":\"torn\",\"file\":\"app/rou{\"type\":\"hit\",\"id\":\"other-clone\"}\n",
2988                    "{\"type\":\"hit\",\"id\":\"after-tear\"}\n",
2989                )
2990                .as_bytes()
2991                .to_vec(),
2992            },
2993        ]);
2994        let report = analyze_coverage_archive(&ArchiveReportRequest {
2995            archive_path: path.clone(),
2996            run_id: "run".into(),
2997            generated_at: "time".into(),
2998            integrity: None,
2999            test_exit_code: ExitCodeInput::Missing,
3000        })
3001        .expect("a torn evidence line must not fail the whole run");
3002        let transport = report.view.transport.expect("transport stats");
3003        assert_eq!(transport.corrupt_records, 2);
3004        assert_eq!(transport.corrupt_files, 2);
3005        assert_eq!(transport.background_server_records, 2);
3006        fs::remove_dir_all(path.parent().unwrap()).unwrap();
3007    }
3008
3009    #[test]
3010    fn archive_analysis_rejects_cross_run_evidence() {
3011        let manifest = CoverageManifest {
3012            unmeasured: Vec::new(),
3013            decisions: vec![],
3014            points: vec![],
3015            branches: vec![],
3016            limitations: vec![],
3017            scope: None,
3018        };
3019        let mut result = raw("test", 0, "passed", &[]);
3020        result.scope = Some(ExecutionScope {
3021            version: 1,
3022            run_id: "other-run".into(),
3023            worker_id: "worker".into(),
3024            test_id: "test".into(),
3025            test_key: "key".into(),
3026            retry: 0,
3027            attempt_id: "attempt".into(),
3028        });
3029        let path = archive(vec![
3030            EvidenceArchiveEntry {
3031                path: "manifest.json".into(),
3032                contents: serde_json::to_vec(&manifest).unwrap(),
3033            },
3034            EvidenceArchiveEntry {
3035                path: "worker/mcdc.json".into(),
3036                contents: serde_json::to_vec(&result).unwrap(),
3037            },
3038        ]);
3039        assert!(matches!(
3040            analyze_coverage_archive(&ArchiveReportRequest {
3041                archive_path: path.clone(),
3042                run_id: "run".into(),
3043                generated_at: "time".into(),
3044                integrity: None,
3045                test_exit_code: ExitCodeInput::Missing,
3046            }),
3047            Err(ReportError::InvalidArchive(reason))
3048                if reason.contains("expected=run actual=other-run")
3049        ));
3050        fs::remove_dir_all(path.parent().unwrap()).unwrap();
3051    }
3052
3053    #[test]
3054    fn coverage_model_vectors_are_strict_and_language_binding_is_fatal() {
3055        let vectors: Value = serde_json::from_str(include_str!(
3056            "../test-assets/coverage-model-v1/vectors.json"
3057        ))
3058        .unwrap();
3059        for value in vectors["valid"].as_array().unwrap() {
3060            let model: PersistedCoverageModel = serde_json::from_value(value.clone()).unwrap();
3061            model.into_declaration().unwrap();
3062        }
3063        for vector in vectors["invalid"].as_array().unwrap() {
3064            let value = vector["value"].clone();
3065            if let Ok(model) = serde_json::from_value::<PersistedCoverageModel>(value) {
3066                assert!(
3067                    model.into_declaration().is_err(),
3068                    "accepted invalid model vector: {}",
3069                    vector["reason"]
3070                );
3071            }
3072        }
3073
3074        let manifest = CoverageManifest {
3075            unmeasured: Vec::new(),
3076            decisions: vec![],
3077            points: vec![],
3078            branches: vec![],
3079            limitations: vec![],
3080            scope: None,
3081        };
3082        let path = archive(vec![
3083            EvidenceArchiveEntry {
3084                path: "coverage-model.json".into(),
3085                contents: serde_json::to_vec(&PersistedCoverageModel {
3086                    schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
3087                    language: "rust".into(),
3088                    variant: "rust-source-v1".into(),
3089                    name: "Rust source coverage".into(),
3090                    completeness_meaning: "Every Rust obligation was satisfied.".into(),
3091                    measured: vec!["Rust statements".into()],
3092                    not_measured: vec![],
3093                })
3094                .unwrap(),
3095            },
3096            EvidenceArchiveEntry {
3097                path: "manifest.json".into(),
3098                contents: serde_json::to_vec(&manifest).unwrap(),
3099            },
3100        ]);
3101        assert!(matches!(
3102            analyze_coverage_archive(&ArchiveReportRequest {
3103                archive_path: path.clone(),
3104                run_id: "run".into(),
3105                generated_at: "time".into(),
3106                integrity: None,
3107                test_exit_code: ExitCodeInput::Missing,
3108            }),
3109            Err(ReportError::InvalidArchive(reason))
3110                if reason.contains("javascript differs from coverage model language rust")
3111        ));
3112        fs::remove_dir_all(path.parent().unwrap()).unwrap();
3113    }
3114
3115    #[test]
3116    fn rust_compiler_scope_requires_an_exact_full_source_fingerprint() {
3117        let mut manifest = CoverageManifest {
3118            unmeasured: Vec::new(),
3119            decisions: vec![],
3120            points: vec![],
3121            branches: vec![],
3122            limitations: vec![],
3123            scope: Some(serde_json::json!({
3124                "language": "rust",
3125                "model": "rust-source-v1",
3126                "crate": "fixture",
3127                "measurementComplete": false,
3128                "sourceFingerprint": {
3129                    "algorithm": "sha256",
3130                    "digest": "1".repeat(64),
3131                    "files": 2,
3132                    "generatedFiles": 1,
3133                },
3134            })),
3135        };
3136        validate_rust_compiler_scope(&manifest).unwrap();
3137
3138        manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3139            Value::String("not-a-digest".into());
3140        assert!(matches!(
3141            validate_rust_compiler_scope(&manifest),
3142            Err(ReportError::InvalidArchive(reason))
3143                if reason == "malformed Rust compiler source fingerprint"
3144        ));
3145
3146        manifest.scope.as_mut().unwrap()["sourceFingerprint"]["digest"] =
3147            Value::String("1".repeat(64));
3148        manifest.scope.as_mut().unwrap()["sourceFingerprint"]["unexpected"] = Value::Bool(true);
3149        assert!(validate_rust_compiler_scope(&manifest).is_err());
3150    }
3151
3152    #[test]
3153    fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
3154        let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
3155            "runId": "run",
3156            "manifest": { "decisions": [], "points": [], "branches": [] },
3157            "rawResults": [{
3158                "test": "test",
3159                "status": "passed",
3160                "browser": [],
3161                "server": []
3162            }],
3163            "generatedAt": "time",
3164            "testExitCode": null
3165        }))
3166        .unwrap();
3167        let report = analyze_coverage_results(&request).unwrap();
3168        assert_eq!(
3169            report.execution,
3170            Some(ExecutionResult {
3171                test_exit_code: None,
3172                valid: false,
3173            })
3174        );
3175    }
3176}