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