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