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