Skip to main content

supercov_engine/
rust_compiler_manifest.rs

1//! Strict ingestion of the private rustc companion's manifest candidate.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use sha2::{Digest, Sha256};
8
9use crate::{
10    coverage_analysis::PointKind,
11    coverage_report::{
12        BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
13    },
14};
15
16const SCHEMA: &str = "supercov-rust-manifest-candidate-v4";
17const MODEL: &str = "rust-source-v1";
18const SOURCE_SNAPSHOT_SCHEMA: &str = "supercov-rust-source-snapshots-v1";
19const SOURCE_FINGERPRINT_DOMAIN: &[u8] = b"supercov-rust-compiler-sources-v1\0";
20
21fn append_fingerprint_field(hash: &mut Sha256, value: &[u8]) {
22    hash.update((value.len() as u64).to_le_bytes());
23    hash.update(value);
24}
25
26fn compiler_source_fingerprint(sources: &BTreeMap<String, RustCompilerSource>) -> (String, usize) {
27    let mut hash = Sha256::new();
28    hash.update(SOURCE_FINGERPRINT_DOMAIN);
29    let mut generated_files = 0;
30    for (key, source) in sources {
31        generated_files += usize::from(key.starts_with("generated:package:"));
32        append_fingerprint_field(&mut hash, key.as_bytes());
33        append_fingerprint_field(&mut hash, source.file.as_bytes());
34        append_fingerprint_field(&mut hash, source.source.as_bytes());
35    }
36    (format!("{:x}", hash.finalize()), generated_files)
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41pub struct RustCompilerManifest {
42    pub schema: String,
43    pub model: String,
44    #[serde(rename = "crate")]
45    pub crate_name: String,
46    pub measurement_complete: bool,
47    /// Bodies whose obligations were actually bound, not merely collected.
48    /// Collected counts come from HIR and do not move when binding stops, so
49    /// this is the number that falls if checking is skipped.
50    #[serde(default)]
51    pub bound_bodies: u64,
52    pub points: Vec<RustCompilerPoint>,
53    pub branches: Vec<RustCompilerBranch>,
54    pub decisions: Vec<RustCompilerDecision>,
55    pub selection_groups: Vec<RustCompilerSelectionGroup>,
56    pub limitations: Vec<String>,
57    /// Obligations the compiler frontend declined to measure exactly.
58    #[serde(default)]
59    pub unmeasured_obligations: Vec<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
63#[serde(rename_all = "camelCase", deny_unknown_fields)]
64pub struct RustCompilerPoint {
65    pub id: String,
66    pub kind: String,
67    pub source_key: String,
68    pub start: u32,
69    pub end: u32,
70    pub provenance: String,
71    pub discriminator: String,
72    pub probe_ordinal: String,
73    pub definitions: Vec<String>,
74    pub canonical: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79pub struct RustCompilerBranchAlternative {
80    pub id: String,
81    pub label: String,
82    pub probe_ordinal: String,
83    pub canonical: String,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase", deny_unknown_fields)]
88pub struct RustCompilerBranch {
89    pub id: String,
90    pub kind: String,
91    pub discriminator: String,
92    pub source_key: String,
93    pub start: u32,
94    pub end: u32,
95    pub provenance: String,
96    pub probe_ordinal: String,
97    pub definitions: Vec<String>,
98    pub alternatives: Vec<RustCompilerBranchAlternative>,
99    pub canonical: String,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
103#[serde(rename_all = "camelCase", deny_unknown_fields)]
104pub struct RustCompilerCondition {
105    pub source_key: String,
106    pub start: u32,
107    pub end: u32,
108    pub source: String,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
112#[serde(rename_all = "camelCase", deny_unknown_fields)]
113pub struct RustCompilerLogicalSelection {
114    pub branch_id: String,
115    pub right_condition_index: usize,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
119#[serde(rename_all = "camelCase", deny_unknown_fields)]
120pub struct RustCompilerDecision {
121    pub id: String,
122    pub kind: String,
123    pub source_key: String,
124    pub start: u32,
125    pub end: u32,
126    pub provenance: String,
127    pub probe_ordinal: String,
128    pub definitions: Vec<String>,
129    pub outcome_branch_id: String,
130    pub loop_branch_id: Option<String>,
131    pub logical_selections: Vec<RustCompilerLogicalSelection>,
132    pub conditions: Vec<RustCompilerCondition>,
133    pub canonical: String,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
137#[serde(rename_all = "camelCase", deny_unknown_fields)]
138pub struct RustCompilerMatchArm {
139    pub branch_id: String,
140    pub body_source_key: String,
141    pub body_start: u32,
142    pub body_end: u32,
143    pub guarded: bool,
144    pub guard_decision_id: Option<String>,
145    pub selected_ordinal: String,
146    pub not_selected_ordinal: String,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase", deny_unknown_fields)]
151pub struct RustCompilerSelectionGroup {
152    pub id: String,
153    pub kind: String,
154    pub source_key: String,
155    pub start: u32,
156    pub end: u32,
157    pub provenance: String,
158    pub probe_ordinal: String,
159    pub definitions: Vec<String>,
160    pub parent_group_id: Option<String>,
161    pub parent_site: Option<String>,
162    pub parent_arm_index: Option<usize>,
163    pub arms: Vec<RustCompilerMatchArm>,
164    pub canonical: String,
165}
166
167/// Source bytes are supplied independently from the compiler manifest so the
168/// engine never resolves a compiler key by guessing at the user's filesystem.
169#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct RustCompilerSource {
172    pub file: String,
173    pub source: String,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
177#[serde(rename_all = "camelCase", deny_unknown_fields)]
178pub struct RustCompilerSourceSnapshots {
179    pub schema: String,
180    #[serde(rename = "crate")]
181    pub crate_name: String,
182    pub sources: BTreeMap<String, RustCompilerSource>,
183}
184
185impl RustCompilerSourceSnapshots {
186    pub fn parse(bytes: &[u8]) -> Result<Self, RustCompilerManifestError> {
187        let snapshots: Self = serde_json::from_slice(bytes)
188            .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
189        snapshots.validate()?;
190        Ok(snapshots)
191    }
192
193    pub fn validate(&self) -> Result<(), RustCompilerManifestError> {
194        self.validate_with_pending_doctest(None)
195    }
196
197    pub(crate) fn parse_pending_doctest(
198        bytes: &[u8],
199        group: &str,
200    ) -> Result<Self, RustCompilerManifestError> {
201        let snapshots: Self = serde_json::from_slice(bytes)
202            .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
203        snapshots.validate_with_pending_doctest(Some(group))?;
204        Ok(snapshots)
205    }
206
207    fn validate_with_pending_doctest(
208        &self,
209        pending_group: Option<&str>,
210    ) -> Result<(), RustCompilerManifestError> {
211        if self.schema != SOURCE_SNAPSHOT_SCHEMA
212            || self.crate_name.trim().is_empty()
213            || self.sources.is_empty()
214            || self.sources.iter().any(|(key, source)| {
215                !valid_source_key_for(key, pending_group)
216                    || source.file.trim().is_empty()
217                    || source.file.chars().any(char::is_control)
218            })
219        {
220            return Err(RustCompilerManifestError::InvalidSource(
221                "malformed compiler source snapshot envelope".into(),
222            ));
223        }
224        if let Some(group) = pending_group {
225            let key = format!("doctest-pending:{group}");
226            if !self.crate_name.starts_with("doctest_bundle_")
227                || self.sources.len() != 1
228                || self
229                    .sources
230                    .get(&key)
231                    .is_none_or(|source| source.file != key)
232            {
233                return Err(RustCompilerManifestError::InvalidSource(
234                    "malformed pending merged-doctest source envelope".into(),
235                ));
236            }
237        }
238        Ok(())
239    }
240}
241
242/// Runtime ordinal semantics retained alongside the language-neutral
243/// manifest. A single selected match arm can cover its selected alternative
244/// and the not-selected alternatives of every sibling in the evaluated group.
245#[derive(Debug, Clone, PartialEq, Serialize)]
246#[serde(rename_all = "camelCase")]
247pub struct NormalizedRustCompilerManifest {
248    pub manifest: CoverageManifest,
249    pub hit_obligations_by_ordinal: BTreeMap<u64, Vec<String>>,
250    pub internal_ordinals: BTreeSet<u64>,
251    pub decision_outcome_obligations: BTreeMap<String, (String, String)>,
252    pub decision_loop_obligations: BTreeMap<String, (String, String)>,
253    pub decision_logical_selection_obligations:
254        BTreeMap<String, Vec<NormalizedRustLogicalSelection>>,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct NormalizedRustLogicalSelection {
260    pub short_circuited_id: String,
261    pub right_evaluated_id: String,
262    pub right_condition_index: usize,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
266#[serde(rename_all = "camelCase", deny_unknown_fields)]
267pub struct RustCompilerNormalizationRequest {
268    pub manifest: RustCompilerManifest,
269    pub sources: BTreeMap<String, RustCompilerSource>,
270}
271
272impl RustCompilerNormalizationRequest {
273    pub fn parse_and_normalize(
274        bytes: &[u8],
275    ) -> Result<NormalizedRustCompilerManifest, RustCompilerManifestError> {
276        let request: Self = serde_json::from_slice(bytes)
277            .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
278        request.manifest.normalize(&request.sources)
279    }
280}
281
282pub fn normalize_rust_compiler_candidates(
283    candidates: Vec<(RustCompilerManifest, RustCompilerSourceSnapshots)>,
284) -> Result<NormalizedRustCompilerManifest, RustCompilerManifestError> {
285    if candidates.is_empty() {
286        return Err(RustCompilerManifestError::Invalid(
287            "compiler build emitted no owned Rust denominator".into(),
288        ));
289    }
290    let mut crate_names = BTreeSet::new();
291    let mut points = BTreeMap::<String, RustCompilerPoint>::new();
292    let mut branches = BTreeMap::<String, RustCompilerBranch>::new();
293    let mut decisions = BTreeMap::<String, RustCompilerDecision>::new();
294    let mut groups = BTreeMap::<String, RustCompilerSelectionGroup>::new();
295    let mut limitations = BTreeSet::new();
296    let mut unmeasured_obligations = BTreeSet::new();
297    let mut sources = BTreeMap::<String, RustCompilerSource>::new();
298    let mut bound_bodies = 0_u64;
299    for (manifest, snapshots) in candidates {
300        manifest.validate()?;
301        bound_bodies = bound_bodies.saturating_add(manifest.bound_bodies);
302        snapshots.validate()?;
303        if snapshots.crate_name != manifest.crate_name {
304            return Err(RustCompilerManifestError::InvalidSource(format!(
305                "compiler manifest/source snapshot identity mismatch for {}",
306                manifest.crate_name
307            )));
308        }
309        crate_names.insert(manifest.crate_name);
310        limitations.extend(manifest.limitations);
311        unmeasured_obligations.extend(manifest.unmeasured_obligations);
312        for (key, source) in snapshots.sources {
313            if let Some(existing) = sources.insert(key.clone(), source.clone())
314                && existing != source
315            {
316                return Err(RustCompilerManifestError::InvalidSource(format!(
317                    "compiler source {key} changed across build units"
318                )));
319            }
320        }
321        for point in manifest.points {
322            merge_point(&mut points, point)?;
323        }
324        for branch in manifest.branches {
325            merge_branch(&mut branches, branch)?;
326        }
327        for decision in manifest.decisions {
328            merge_decision(&mut decisions, decision)?;
329        }
330        for group in manifest.selection_groups {
331            merge_selection_group(&mut groups, group)?;
332        }
333    }
334    let manifest = RustCompilerManifest {
335        schema: SCHEMA.into(),
336        model: MODEL.into(),
337        crate_name: if crate_names.len() == 1 {
338            crate_names.into_iter().next().expect("one crate")
339        } else {
340            "workspace".into()
341        },
342        measurement_complete: false,
343        bound_bodies,
344        points: points.into_values().collect(),
345        branches: branches.into_values().collect(),
346        decisions: decisions.into_values().collect(),
347        selection_groups: groups.into_values().collect(),
348        limitations: limitations.into_iter().collect(),
349        unmeasured_obligations: unmeasured_obligations.into_iter().collect(),
350    };
351    manifest.normalize(&sources)
352}
353
354fn merge_definitions(destination: &mut Vec<String>, source: Vec<String>) {
355    destination.extend(source);
356    destination.sort();
357    destination.dedup();
358}
359
360fn merge_point(
361    destination: &mut BTreeMap<String, RustCompilerPoint>,
362    point: RustCompilerPoint,
363) -> Result<(), RustCompilerManifestError> {
364    if let Some(existing) = destination.get_mut(&point.id) {
365        let mut left = existing.clone();
366        let mut right = point.clone();
367        left.definitions.clear();
368        right.definitions.clear();
369        if left != right {
370            return Err(RustCompilerManifestError::Invalid(format!(
371                "point {} changed across build units",
372                point.id
373            )));
374        }
375        merge_definitions(&mut existing.definitions, point.definitions);
376    } else {
377        destination.insert(point.id.clone(), point);
378    }
379    Ok(())
380}
381
382fn merge_branch(
383    destination: &mut BTreeMap<String, RustCompilerBranch>,
384    branch: RustCompilerBranch,
385) -> Result<(), RustCompilerManifestError> {
386    if let Some(existing) = destination.get_mut(&branch.id) {
387        let mut left = existing.clone();
388        let mut right = branch.clone();
389        left.definitions.clear();
390        right.definitions.clear();
391        if left != right {
392            return Err(RustCompilerManifestError::Invalid(format!(
393                "branch {} changed across build units",
394                branch.id
395            )));
396        }
397        merge_definitions(&mut existing.definitions, branch.definitions);
398    } else {
399        destination.insert(branch.id.clone(), branch);
400    }
401    Ok(())
402}
403
404fn merge_decision(
405    destination: &mut BTreeMap<String, RustCompilerDecision>,
406    decision: RustCompilerDecision,
407) -> Result<(), RustCompilerManifestError> {
408    if let Some(existing) = destination.get_mut(&decision.id) {
409        let mut left = existing.clone();
410        let mut right = decision.clone();
411        left.definitions.clear();
412        right.definitions.clear();
413        if left != right {
414            return Err(RustCompilerManifestError::Invalid(format!(
415                "decision {} changed across build units",
416                decision.id
417            )));
418        }
419        merge_definitions(&mut existing.definitions, decision.definitions);
420    } else {
421        destination.insert(decision.id.clone(), decision);
422    }
423    Ok(())
424}
425
426fn merge_selection_group(
427    destination: &mut BTreeMap<String, RustCompilerSelectionGroup>,
428    group: RustCompilerSelectionGroup,
429) -> Result<(), RustCompilerManifestError> {
430    if let Some(existing) = destination.get_mut(&group.id) {
431        let mut left = existing.clone();
432        let mut right = group.clone();
433        left.definitions.clear();
434        right.definitions.clear();
435        if left != right {
436            return Err(RustCompilerManifestError::Invalid(format!(
437                "selection group {} changed across build units",
438                group.id
439            )));
440        }
441        merge_definitions(&mut existing.definitions, group.definitions);
442    } else {
443        destination.insert(group.id.clone(), group);
444    }
445    Ok(())
446}
447
448#[derive(Debug, Clone, PartialEq, Eq)]
449pub enum RustCompilerManifestError {
450    Json(String),
451    Invalid(String),
452    MissingSource(String),
453    InvalidSource(String),
454}
455
456impl std::fmt::Display for RustCompilerManifestError {
457    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        match self {
459            Self::Json(error) => write!(formatter, "invalid Rust compiler manifest JSON: {error}"),
460            Self::Invalid(error) => write!(formatter, "invalid Rust compiler manifest: {error}"),
461            Self::MissingSource(key) => {
462                write!(
463                    formatter,
464                    "Rust compiler manifest source {key} was not supplied"
465                )
466            }
467            Self::InvalidSource(error) => {
468                write!(formatter, "invalid Rust compiler manifest source: {error}")
469            }
470        }
471    }
472}
473
474impl std::error::Error for RustCompilerManifestError {}
475
476fn sorted_unique_nonempty(values: &[String]) -> bool {
477    !values.is_empty()
478        && values.iter().all(|value| !value.trim().is_empty())
479        && values.windows(2).all(|pair| pair[0] < pair[1])
480}
481
482fn valid_id(id: &str, allowed: &[&str]) -> bool {
483    let mut parts = id.split(':');
484    parts.next() == Some("rs")
485        && parts.next().is_some_and(|kind| allowed.contains(&kind))
486        && parts.next().is_some_and(|digest| {
487            digest.len() == 24 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
488        })
489        && parts.next().is_none()
490}
491
492fn normalized_relative_path(value: &str, allow_package_root: bool) -> bool {
493    if allow_package_root && value == "." {
494        return true;
495    }
496    !value.is_empty()
497        && !value.starts_with('/')
498        && !value.contains('\\')
499        && value
500            .split('/')
501            .all(|component| !component.is_empty() && !matches!(component, "." | ".."))
502}
503
504fn valid_source_key(key: &str) -> bool {
505    if let Some(path) = key.strip_prefix("source:") {
506        return normalized_relative_path(path, false);
507    }
508    let Some(generated) = key.strip_prefix("generated:package:") else {
509        return false;
510    };
511    let Some((package, output)) = generated.split_once(':') else {
512        return false;
513    };
514    normalized_relative_path(package, true) && normalized_relative_path(output, false)
515}
516
517fn valid_source_key_for(key: &str, pending_group: Option<&str>) -> bool {
518    valid_source_key(key)
519        || pending_group.is_some_and(|group| key == format!("doctest-pending:{group}"))
520}
521
522fn source_range_for(key: &str, start: u32, end: u32, pending_group: Option<&str>) -> bool {
523    valid_source_key_for(key, pending_group) && start < end
524}
525
526fn ordinal(value: &str) -> Option<u64> {
527    value.parse::<u64>().ok().filter(|ordinal| *ordinal != 0)
528}
529
530fn provenance_for(value: &str, pending_group: Option<&str>) -> bool {
531    matches!(
532        value,
533        "authored-source"
534            | "authored-expansion"
535            | "synthetic-expansion"
536            | "generated-source"
537            | "doctest-source"
538    ) || pending_group.is_some() && value == "doctest-pending"
539}
540
541fn insert_identity(
542    ids: &mut BTreeSet<String>,
543    ordinals: &mut BTreeSet<u64>,
544    id: &str,
545    probe_ordinal: &str,
546) -> Result<(), RustCompilerManifestError> {
547    if !ids.insert(id.into()) {
548        return Err(RustCompilerManifestError::Invalid(format!(
549            "duplicate obligation ID {id}"
550        )));
551    }
552    let ordinal = ordinal(probe_ordinal).ok_or_else(|| {
553        RustCompilerManifestError::Invalid(format!(
554            "obligation {id} has invalid probe ordinal {probe_ordinal}"
555        ))
556    })?;
557    if !ordinals.insert(ordinal) {
558        return Err(RustCompilerManifestError::Invalid(format!(
559            "duplicate probe ordinal {ordinal}"
560        )));
561    }
562    Ok(())
563}
564
565impl RustCompilerManifest {
566    pub fn parse(bytes: &[u8]) -> Result<Self, RustCompilerManifestError> {
567        let manifest: Self = serde_json::from_slice(bytes)
568            .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
569        manifest.validate()?;
570        Ok(manifest)
571    }
572
573    pub fn validate(&self) -> Result<(), RustCompilerManifestError> {
574        self.validate_with_pending_doctest(None)
575    }
576
577    pub(crate) fn parse_pending_doctest(
578        bytes: &[u8],
579        group: &str,
580    ) -> Result<Self, RustCompilerManifestError> {
581        let manifest: Self = serde_json::from_slice(bytes)
582            .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
583        manifest.validate_with_pending_doctest(Some(group))?;
584        Ok(manifest)
585    }
586
587    fn validate_with_pending_doctest(
588        &self,
589        pending_group: Option<&str>,
590    ) -> Result<(), RustCompilerManifestError> {
591        let invalid = |reason: &str| RustCompilerManifestError::Invalid(reason.into());
592        if self.schema != SCHEMA || self.model != MODEL {
593            return Err(invalid("unsupported schema or source model"));
594        }
595        if self.crate_name.trim().is_empty() || self.measurement_complete {
596            return Err(invalid(
597                "a private candidate needs a crate identity and cannot claim completeness",
598            ));
599        }
600        if let Some(group) = pending_group
601            && (!self.crate_name.starts_with("doctest_bundle_")
602                || group.is_empty()
603                || !group
604                    .bytes()
605                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')))
606        {
607            return Err(invalid("malformed pending merged-doctest identity"));
608        }
609        if self.points.is_empty() || !sorted_unique_nonempty(&self.limitations) {
610            return Err(invalid(
611                "a candidate needs points and sorted explicit limitations",
612            ));
613        }
614        if !self.points.windows(2).all(|pair| pair[0].id < pair[1].id)
615            || !self.branches.windows(2).all(|pair| pair[0].id < pair[1].id)
616            || !self
617                .decisions
618                .windows(2)
619                .all(|pair| pair[0].id < pair[1].id)
620            || !self
621                .selection_groups
622                .windows(2)
623                .all(|pair| pair[0].id < pair[1].id)
624        {
625            return Err(invalid("obligation arrays are not in canonical ID order"));
626        }
627        if let Some(group) = pending_group {
628            let key = format!("doctest-pending:{group}");
629            let exact_pending = |source_key: &str, provenance: &str| {
630                source_key == key && provenance == "doctest-pending"
631            };
632            if self
633                .points
634                .iter()
635                .any(|point| !exact_pending(&point.source_key, &point.provenance))
636                || self
637                    .branches
638                    .iter()
639                    .any(|branch| !exact_pending(&branch.source_key, &branch.provenance))
640                || self.decisions.iter().any(|decision| {
641                    !exact_pending(&decision.source_key, &decision.provenance)
642                        || decision
643                            .conditions
644                            .iter()
645                            .any(|condition| condition.source_key != key)
646                })
647                || self.selection_groups.iter().any(|selection| {
648                    !exact_pending(&selection.source_key, &selection.provenance)
649                        || selection.arms.iter().any(|arm| arm.body_source_key != key)
650                })
651            {
652                return Err(invalid(
653                    "pending merged-doctest manifest mixes final and temporary source identities",
654                ));
655            }
656        }
657
658        let mut ids = BTreeSet::new();
659        let mut ordinals = BTreeSet::new();
660        for point in &self.points {
661            if !valid_id(&point.id, &["statement", "function"])
662                || !matches!(point.kind.as_str(), "statement" | "function")
663                || !source_range_for(&point.source_key, point.start, point.end, pending_group)
664                || !provenance_for(&point.provenance, pending_group)
665                || !sorted_unique_nonempty(&point.definitions)
666                || point.canonical.is_empty()
667            {
668                return Err(invalid("malformed point obligation"));
669            }
670            insert_identity(&mut ids, &mut ordinals, &point.id, &point.probe_ordinal)?;
671        }
672        let mut branch_ids = BTreeSet::new();
673        for branch in &self.branches {
674            if !valid_id(&branch.id, &["branch"])
675                || !matches!(
676                    branch.kind.as_str(),
677                    "decision-outcome"
678                        | "loop-entry"
679                        | "match-arm"
680                        | "let-else"
681                        | "try-operator"
682                        | "assertion-outcome"
683                        | "logical-selection"
684                )
685                || !source_range_for(&branch.source_key, branch.start, branch.end, pending_group)
686                || !provenance_for(&branch.provenance, pending_group)
687                || !sorted_unique_nonempty(&branch.definitions)
688                || branch.alternatives.len() < 2
689                || (branch.kind == "logical-selection"
690                    && !matches!(
691                        branch.discriminator.as_str(),
692                        "logical-selection:and" | "logical-selection:or"
693                    ))
694                || branch.canonical.is_empty()
695            {
696                return Err(invalid("malformed branch obligation"));
697            }
698            insert_identity(&mut ids, &mut ordinals, &branch.id, &branch.probe_ordinal)?;
699            branch_ids.insert(branch.id.as_str());
700            let mut labels = BTreeSet::new();
701            for alternative in &branch.alternatives {
702                if !valid_id(&alternative.id, &["branch-alternative"])
703                    || alternative.label.trim().is_empty()
704                    || alternative.canonical.is_empty()
705                    || !labels.insert(alternative.label.as_str())
706                {
707                    return Err(invalid("malformed branch alternative"));
708                }
709                insert_identity(
710                    &mut ids,
711                    &mut ordinals,
712                    &alternative.id,
713                    &alternative.probe_ordinal,
714                )?;
715            }
716        }
717        let mut decision_ids = BTreeSet::new();
718        let mut referenced_logical_branch_ids = BTreeSet::new();
719        for decision in &self.decisions {
720            if !valid_id(&decision.id, &["decision"])
721                || !matches!(
722                    decision.kind.as_str(),
723                    "if" | "if-let"
724                        | "while"
725                        | "while-let"
726                        | "let-chain"
727                        | "match-guard"
728                        | "assertion"
729                )
730                || !source_range_for(
731                    &decision.source_key,
732                    decision.start,
733                    decision.end,
734                    pending_group,
735                )
736                || !provenance_for(&decision.provenance, pending_group)
737                || !sorted_unique_nonempty(&decision.definitions)
738                || decision.conditions.is_empty()
739                || !decision
740                    .logical_selections
741                    .windows(2)
742                    .all(|pair| pair[0].right_condition_index < pair[1].right_condition_index)
743                || decision.canonical.is_empty()
744                || decision.conditions.iter().any(|condition| {
745                    !source_range_for(
746                        &condition.source_key,
747                        condition.start,
748                        condition.end,
749                        pending_group,
750                    ) || condition.source.trim().is_empty()
751                })
752            {
753                return Err(invalid("malformed decision obligation"));
754            }
755            for selection in &decision.logical_selections {
756                if selection.right_condition_index == 0
757                    || selection.right_condition_index >= decision.conditions.len()
758                    || !referenced_logical_branch_ids.insert(selection.branch_id.as_str())
759                {
760                    return Err(invalid("malformed logical-selection relation"));
761                }
762                let Some(branch) = self
763                    .branches
764                    .iter()
765                    .find(|branch| branch.id == selection.branch_id)
766                else {
767                    return Err(invalid(
768                        "decision references a missing logical-selection branch",
769                    ));
770                };
771                if branch.kind != "logical-selection"
772                    || branch
773                        .alternatives
774                        .iter()
775                        .map(|alternative| alternative.label.as_str())
776                        .collect::<BTreeSet<_>>()
777                        != BTreeSet::from(["right operand evaluated", "short-circuited"])
778                {
779                    return Err(invalid("decision has a malformed logical-selection branch"));
780                }
781            }
782            let Some(outcome_branch) = self
783                .branches
784                .iter()
785                .find(|branch| branch.id == decision.outcome_branch_id)
786            else {
787                return Err(invalid("decision references a missing outcome branch"));
788            };
789            let expected_kind = if decision.kind == "assertion" {
790                "assertion-outcome"
791            } else {
792                "decision-outcome"
793            };
794            let expected_labels = if decision.kind == "assertion" {
795                BTreeSet::from(["failed", "passed"])
796            } else {
797                BTreeSet::from(["condition false", "condition true"])
798            };
799            if outcome_branch.kind != expected_kind
800                || outcome_branch
801                    .alternatives
802                    .iter()
803                    .map(|alternative| alternative.label.as_str())
804                    .collect::<BTreeSet<_>>()
805                    != expected_labels
806            {
807                return Err(invalid("decision has a malformed outcome branch"));
808            }
809            match (
810                decision.kind.starts_with("while"),
811                decision.loop_branch_id.as_deref(),
812            ) {
813                (true, Some(loop_branch_id)) => {
814                    let Some(loop_branch) = self
815                        .branches
816                        .iter()
817                        .find(|branch| branch.id == loop_branch_id)
818                    else {
819                        return Err(invalid("decision references a missing loop-entry branch"));
820                    };
821                    if loop_branch.kind != "loop-entry"
822                        || loop_branch
823                            .alternatives
824                            .iter()
825                            .map(|alternative| alternative.label.as_str())
826                            .collect::<BTreeSet<_>>()
827                            != BTreeSet::from(["entered", "zero iterations"])
828                    {
829                        return Err(invalid("decision has a malformed loop-entry branch"));
830                    }
831                }
832                (true, None) => {
833                    return Err(invalid("while decision lacks an exact loop-entry branch"));
834                }
835                (false, Some(_)) => {
836                    return Err(invalid("non-while decision references a loop-entry branch"));
837                }
838                (false, None) => {}
839            }
840            insert_identity(
841                &mut ids,
842                &mut ordinals,
843                &decision.id,
844                &decision.probe_ordinal,
845            )?;
846            decision_ids.insert(decision.id.as_str());
847        }
848        let selection_ids = self
849            .selection_groups
850            .iter()
851            .map(|group| group.id.as_str())
852            .collect::<BTreeSet<_>>();
853        if selection_ids.len() != self.selection_groups.len() {
854            return Err(invalid("duplicate match selection group ID"));
855        }
856        let mut grouped_branch_ids = BTreeSet::new();
857        for group in &self.selection_groups {
858            if !valid_id(&group.id, &["match-group"])
859                || group.kind != "match"
860                || !source_range_for(&group.source_key, group.start, group.end, pending_group)
861                || !provenance_for(&group.provenance, pending_group)
862                || !sorted_unique_nonempty(&group.definitions)
863                || group.arms.len() < 2
864                || group.canonical.is_empty()
865                || group
866                    .parent_group_id
867                    .as_ref()
868                    .is_some_and(|parent| !selection_ids.contains(parent.as_str()))
869                || group.parent_site.is_some() != group.parent_group_id.is_some()
870                || group
871                    .parent_site
872                    .as_deref()
873                    .is_some_and(|site| !matches!(site, "scrutinee" | "guard" | "body"))
874                || match group.parent_site.as_deref() {
875                    Some("scrutinee") => group.parent_arm_index.is_some(),
876                    Some("guard" | "body") => group.parent_arm_index.is_none(),
877                    _ => false,
878                }
879            {
880                return Err(RustCompilerManifestError::Invalid(format!(
881                    "malformed match selection group {}: id={} kind={} range={} provenance={} definitions={} arms={} canonical={} parent={} site={} arm={}",
882                    group.id,
883                    valid_id(&group.id, &["match-group"]),
884                    group.kind == "match",
885                    source_range_for(&group.source_key, group.start, group.end, pending_group,),
886                    provenance_for(&group.provenance, pending_group),
887                    sorted_unique_nonempty(&group.definitions),
888                    group.arms.len(),
889                    !group.canonical.is_empty(),
890                    group.parent_group_id.is_some(),
891                    group.parent_site.is_some(),
892                    group.parent_arm_index.is_some(),
893                )));
894            }
895            insert_identity(&mut ids, &mut ordinals, &group.id, &group.probe_ordinal)?;
896            let mut arm_branches = BTreeSet::new();
897            for arm in &group.arms {
898                if !branch_ids.contains(arm.branch_id.as_str())
899                    || !arm_branches.insert(arm.branch_id.as_str())
900                    || !grouped_branch_ids.insert(arm.branch_id.as_str())
901                    || !source_range_for(
902                        &arm.body_source_key,
903                        arm.body_start,
904                        arm.body_end,
905                        pending_group,
906                    )
907                    || arm.guarded != arm.guard_decision_id.is_some()
908                    || arm
909                        .guard_decision_id
910                        .as_ref()
911                        .is_some_and(|guard| !decision_ids.contains(guard.as_str()))
912                    || ordinal(&arm.selected_ordinal).is_none()
913                    || ordinal(&arm.not_selected_ordinal).is_none()
914                {
915                    return Err(invalid("malformed match arm mapping"));
916                }
917                let branch = self
918                    .branches
919                    .iter()
920                    .find(|branch| branch.id == arm.branch_id)
921                    .expect("validated branch reference");
922                if branch.kind != "match-arm"
923                    || branch.alternatives.len() != 2
924                    || branch
925                        .alternatives
926                        .iter()
927                        .map(|alternative| alternative.label.as_str())
928                        .collect::<BTreeSet<_>>()
929                        != BTreeSet::from(["not selected", "selected"])
930                {
931                    return Err(invalid("match group references a non-match branch"));
932                }
933                let alternatives = branch
934                    .alternatives
935                    .iter()
936                    .map(|alternative| alternative.probe_ordinal.as_str())
937                    .collect::<BTreeSet<_>>();
938                if alternatives
939                    != BTreeSet::from([
940                        arm.selected_ordinal.as_str(),
941                        arm.not_selected_ordinal.as_str(),
942                    ])
943                {
944                    return Err(invalid("match arm ordinals do not match its branch"));
945                }
946            }
947        }
948        for group in &self.selection_groups {
949            let mut visited = BTreeSet::new();
950            let mut current = group;
951            while let Some(parent_id) = &current.parent_group_id {
952                if !visited.insert(current.id.as_str()) {
953                    return Err(invalid("cyclic match selection group parentage"));
954                }
955                let parent = self
956                    .selection_groups
957                    .iter()
958                    .find(|candidate| candidate.id == *parent_id)
959                    .expect("validated parent group reference");
960                if current
961                    .parent_arm_index
962                    .is_some_and(|index| index >= parent.arms.len())
963                {
964                    return Err(invalid("match selection parent arm is out of range"));
965                }
966                current = parent;
967            }
968        }
969        Ok(())
970    }
971
972    /// Convert the compiler-owned denominator into Supercov's shared manifest
973    /// and exact ordinal resolver. This is intentionally strict: source keys,
974    /// byte ranges and UTF-8 boundaries must all resolve before any evidence is
975    /// accepted for the crate.
976    pub fn normalize(
977        &self,
978        sources: &BTreeMap<String, RustCompilerSource>,
979    ) -> Result<NormalizedRustCompilerManifest, RustCompilerManifestError> {
980        self.validate()?;
981        let required_source_keys = self
982            .points
983            .iter()
984            .map(|point| point.source_key.as_str())
985            .chain(
986                self.branches
987                    .iter()
988                    .map(|branch| branch.source_key.as_str()),
989            )
990            .chain(self.decisions.iter().flat_map(|decision| {
991                std::iter::once(decision.source_key.as_str()).chain(
992                    decision
993                        .conditions
994                        .iter()
995                        .map(|condition| condition.source_key.as_str()),
996                )
997            }))
998            .chain(self.selection_groups.iter().flat_map(|group| {
999                std::iter::once(group.source_key.as_str())
1000                    .chain(group.arms.iter().map(|arm| arm.body_source_key.as_str()))
1001            }))
1002            .collect::<BTreeSet<_>>();
1003        let supplied_source_keys = sources.keys().map(String::as_str).collect::<BTreeSet<_>>();
1004        if supplied_source_keys != required_source_keys {
1005            let missing = required_source_keys
1006                .difference(&supplied_source_keys)
1007                .copied()
1008                .collect::<Vec<_>>();
1009            let extra = supplied_source_keys
1010                .difference(&required_source_keys)
1011                .copied()
1012                .collect::<Vec<_>>();
1013            return Err(RustCompilerManifestError::InvalidSource(format!(
1014                "source snapshot keys differ from the denominator (missing: {}; extra: {})",
1015                missing.join(", "),
1016                extra.join(", ")
1017            )));
1018        }
1019        let location = |key: &str, start: u32, end: u32| source_location(sources, key, start, end);
1020
1021        let mut hit_obligations_by_ordinal = BTreeMap::<u64, BTreeSet<String>>::new();
1022        let mut internal_ordinals = BTreeSet::new();
1023        let mut points = Vec::with_capacity(self.points.len());
1024        for point in &self.points {
1025            let (file, line, column, source) = location(&point.source_key, point.start, point.end)?;
1026            let point_kind = match point.kind.as_str() {
1027                "statement" => PointKind::Statement,
1028                "function" => PointKind::Function,
1029                _ => unreachable!("validated point kind"),
1030            };
1031            let probe_ordinal = ordinal(&point.probe_ordinal).expect("validated point ordinal");
1032            hit_obligations_by_ordinal
1033                .entry(probe_ordinal)
1034                .or_default()
1035                .insert(point.id.clone());
1036            points.push(PointMeta {
1037                id: point.id.clone(),
1038                kind: point_kind,
1039                file,
1040                line,
1041                column,
1042                source,
1043                label: (!point.discriminator.is_empty()).then(|| point.discriminator.clone()),
1044            });
1045        }
1046
1047        let decision_logical_branch_ids = self
1048            .decisions
1049            .iter()
1050            .flat_map(|decision| {
1051                decision
1052                    .logical_selections
1053                    .iter()
1054                    .map(|selection| selection.branch_id.as_str())
1055            })
1056            .collect::<BTreeSet<_>>();
1057        let mut branches = Vec::with_capacity(self.branches.len());
1058        for branch in &self.branches {
1059            let (file, line, column, source) =
1060                location(&branch.source_key, branch.start, branch.end)?;
1061            internal_ordinals
1062                .insert(ordinal(&branch.probe_ordinal).expect("validated branch group ordinal"));
1063            let alternatives = branch
1064                .alternatives
1065                .iter()
1066                .map(|alternative| {
1067                    let probe_ordinal = ordinal(&alternative.probe_ordinal)
1068                        .expect("validated branch alternative ordinal");
1069                    if decision_logical_branch_ids.contains(branch.id.as_str()) {
1070                        internal_ordinals.insert(probe_ordinal);
1071                    } else {
1072                        hit_obligations_by_ordinal
1073                            .entry(probe_ordinal)
1074                            .or_default()
1075                            .insert(alternative.id.clone());
1076                    }
1077                    BranchAlternativeMeta {
1078                        id: alternative.id.clone(),
1079                        label: alternative.label.clone(),
1080                    }
1081                })
1082                .collect();
1083            branches.push(BranchMeta {
1084                id: branch.id.clone(),
1085                kind: branch.kind.clone(),
1086                file,
1087                line,
1088                column,
1089                source,
1090                alternatives,
1091            });
1092        }
1093
1094        let mut decisions = Vec::with_capacity(self.decisions.len());
1095        let branches_by_id = self
1096            .branches
1097            .iter()
1098            .map(|branch| (branch.id.as_str(), branch))
1099            .collect::<BTreeMap<_, _>>();
1100        let mut decision_outcome_obligations = BTreeMap::new();
1101        let mut decision_loop_obligations = BTreeMap::new();
1102        let mut decision_logical_selection_obligations = BTreeMap::new();
1103        for decision in &self.decisions {
1104            let (file, line, column, source) =
1105                location(&decision.source_key, decision.start, decision.end)?;
1106            for condition in &decision.conditions {
1107                // Resolve even when compiler-reconstructed display text differs
1108                // from the authored span, as can happen for procedural output.
1109                location(&condition.source_key, condition.start, condition.end)?;
1110            }
1111            internal_ordinals
1112                .insert(ordinal(&decision.probe_ordinal).expect("validated decision ordinal"));
1113            let outcome_branch = branches_by_id[decision.outcome_branch_id.as_str()];
1114            let labels = if decision.kind == "assertion" {
1115                ("failed", "passed")
1116            } else {
1117                ("condition false", "condition true")
1118            };
1119            let alternative = |label: &str| {
1120                outcome_branch
1121                    .alternatives
1122                    .iter()
1123                    .find(|alternative| alternative.label == label)
1124                    .expect("validated decision outcome label")
1125                    .id
1126                    .clone()
1127            };
1128            decision_outcome_obligations.insert(
1129                decision.id.clone(),
1130                (alternative(labels.0), alternative(labels.1)),
1131            );
1132            if let Some(loop_branch_id) = decision.loop_branch_id.as_deref() {
1133                let loop_branch = branches_by_id[loop_branch_id];
1134                let loop_alternative = |label: &str| {
1135                    loop_branch
1136                        .alternatives
1137                        .iter()
1138                        .find(|alternative| alternative.label == label)
1139                        .expect("validated loop-entry label")
1140                        .id
1141                        .clone()
1142                };
1143                decision_loop_obligations.insert(
1144                    decision.id.clone(),
1145                    (
1146                        loop_alternative("zero iterations"),
1147                        loop_alternative("entered"),
1148                    ),
1149                );
1150            }
1151            let logical_selections = decision
1152                .logical_selections
1153                .iter()
1154                .map(|selection| {
1155                    let branch = branches_by_id[selection.branch_id.as_str()];
1156                    let alternative = |label: &str| {
1157                        branch
1158                            .alternatives
1159                            .iter()
1160                            .find(|alternative| alternative.label == label)
1161                            .expect("validated logical-selection label")
1162                            .id
1163                            .clone()
1164                    };
1165                    NormalizedRustLogicalSelection {
1166                        short_circuited_id: alternative("short-circuited"),
1167                        right_evaluated_id: alternative("right operand evaluated"),
1168                        right_condition_index: selection.right_condition_index,
1169                    }
1170                })
1171                .collect::<Vec<_>>();
1172            if !logical_selections.is_empty() {
1173                decision_logical_selection_obligations
1174                    .insert(decision.id.clone(), logical_selections);
1175            }
1176            decisions.push(DecisionMeta {
1177                id: decision.id.clone(),
1178                file,
1179                line,
1180                column,
1181                source,
1182                conditions: decision
1183                    .conditions
1184                    .iter()
1185                    .map(|condition| condition.source.clone())
1186                    .collect(),
1187                kind: decision.kind.clone(),
1188            });
1189        }
1190
1191        for group in &self.selection_groups {
1192            internal_ordinals
1193                .insert(ordinal(&group.probe_ordinal).expect("validated selection group ordinal"));
1194            for selected in &group.arms {
1195                let selected_ordinal =
1196                    ordinal(&selected.selected_ordinal).expect("validated selected ordinal");
1197                let implied = hit_obligations_by_ordinal
1198                    .get_mut(&selected_ordinal)
1199                    .expect("validated selected alternative ordinal");
1200                for sibling in &group.arms {
1201                    if sibling.branch_id == selected.branch_id {
1202                        continue;
1203                    }
1204                    let sibling_branch = branches_by_id
1205                        .get(sibling.branch_id.as_str())
1206                        .expect("validated match branch");
1207                    let not_selected = sibling_branch
1208                        .alternatives
1209                        .iter()
1210                        .find(|alternative| {
1211                            alternative.probe_ordinal == sibling.not_selected_ordinal
1212                        })
1213                        .expect("validated not-selected alternative");
1214                    implied.insert(not_selected.id.clone());
1215                }
1216            }
1217        }
1218
1219        let limitation_file = points
1220            .first()
1221            .map(|point| point.file.clone())
1222            .unwrap_or_default();
1223        let limitations = self
1224            .limitations
1225            .iter()
1226            .enumerate()
1227            .map(|(index, limitation)| {
1228                json!({
1229                    "id": format!("rust-compiler-candidate:{index}"),
1230                    "kind": "rust-compiler-candidate",
1231                    "file": limitation_file,
1232                    "line": 1,
1233                    "column": 0,
1234                    "source": "",
1235                    "reason": limitation,
1236                })
1237            })
1238            .collect();
1239
1240        let (source_fingerprint, generated_source_files) = compiler_source_fingerprint(sources);
1241        Ok(NormalizedRustCompilerManifest {
1242            manifest: CoverageManifest {
1243                decisions,
1244                points,
1245                branches,
1246                limitations,
1247                unmeasured: self.unmeasured_obligations.clone(),
1248                scope: Some(json!({
1249                    "language": "rust",
1250                    "model": self.model,
1251                    "crate": self.crate_name,
1252                    "measurementComplete": self.measurement_complete,
1253                    "sourceFingerprint": {
1254                        "algorithm": "sha256",
1255                        "digest": source_fingerprint,
1256                        "files": sources.len(),
1257                        "generatedFiles": generated_source_files,
1258                    },
1259                })),
1260            },
1261            hit_obligations_by_ordinal: hit_obligations_by_ordinal
1262                .into_iter()
1263                .map(|(ordinal, ids)| (ordinal, ids.into_iter().collect()))
1264                .collect(),
1265            internal_ordinals,
1266            decision_outcome_obligations,
1267            decision_loop_obligations,
1268            decision_logical_selection_obligations,
1269        })
1270    }
1271}
1272
1273fn source_location(
1274    sources: &BTreeMap<String, RustCompilerSource>,
1275    key: &str,
1276    start: u32,
1277    end: u32,
1278) -> Result<(String, usize, usize, String), RustCompilerManifestError> {
1279    let source = sources
1280        .get(key)
1281        .ok_or_else(|| RustCompilerManifestError::MissingSource(key.into()))?;
1282    if source.file.trim().is_empty() {
1283        return Err(RustCompilerManifestError::InvalidSource(format!(
1284            "{key} has an empty display path"
1285        )));
1286    }
1287    let start = usize::try_from(start).expect("u32 always fits supported usize");
1288    let end = usize::try_from(end).expect("u32 always fits supported usize");
1289    if start >= end
1290        || end > source.source.len()
1291        || !source.source.is_char_boundary(start)
1292        || !source.source.is_char_boundary(end)
1293    {
1294        return Err(RustCompilerManifestError::InvalidSource(format!(
1295            "{key} range {start}..{end} is outside UTF-8 source bytes"
1296        )));
1297    }
1298    let line_start = source.source[..start]
1299        .rfind('\n')
1300        .map_or(0, |index| index + 1);
1301    let line = source.source[..start]
1302        .bytes()
1303        .filter(|byte| *byte == b'\n')
1304        .count()
1305        + 1;
1306    Ok((
1307        source.file.clone(),
1308        line,
1309        start - line_start,
1310        source.source[start..end].into(),
1311    ))
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316    use serde_json::json;
1317
1318    use super::*;
1319
1320    fn valid_manifest() -> serde_json::Value {
1321        json!({
1322            "schema": SCHEMA,
1323            "model": MODEL,
1324            "crate": "fixture",
1325            "measurementComplete": false,
1326            "points": [{
1327                "id": "rs:function:000000000000000000000001",
1328                "kind": "function",
1329                "sourceKey": "source:src/lib.rs",
1330                "start": 0,
1331                "end": 10,
1332                "provenance": "authored-source",
1333                "discriminator": "",
1334                "probeOrdinal": "1",
1335                "definitions": ["fixture::function"],
1336                "canonical": "function"
1337            }],
1338            "branches": [{
1339                "id": "rs:branch:000000000000000000000002",
1340                "kind": "decision-outcome",
1341                "discriminator": "decision-outcome:if",
1342                "sourceKey": "source:src/lib.rs",
1343                "start": 11,
1344                "end": 20,
1345                "provenance": "authored-source",
1346                "probeOrdinal": "2",
1347                "definitions": ["fixture::function"],
1348                "alternatives": [
1349                    {"id": "rs:branch-alternative:000000000000000000000003", "label": "condition true", "probeOrdinal": "3", "canonical": "true"},
1350                    {"id": "rs:branch-alternative:000000000000000000000004", "label": "condition false", "probeOrdinal": "4", "canonical": "false"}
1351                ],
1352                "canonical": "branch"
1353            }],
1354            "decisions": [{
1355                "id": "rs:decision:000000000000000000000005",
1356                "kind": "if",
1357                "sourceKey": "source:src/lib.rs",
1358                "start": 11,
1359                "end": 15,
1360                "provenance": "authored-source",
1361                "probeOrdinal": "5",
1362                "definitions": ["fixture::function"],
1363                "outcomeBranchId": "rs:branch:000000000000000000000002",
1364                "loopBranchId": null,
1365                "logicalSelections": [],
1366                "conditions": [{"sourceKey": "source:src/lib.rs", "start": 11, "end": 15, "source": "value"}],
1367                "canonical": "decision"
1368            }],
1369            "selectionGroups": [],
1370            "limitations": ["RUST_PRIVATE_CANDIDATE: incomplete"]
1371        })
1372    }
1373
1374    #[test]
1375    fn accepts_only_a_strict_collision_free_private_candidate() {
1376        let manifest =
1377            RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1378        assert_eq!(manifest.crate_name, "fixture");
1379
1380        let mut legacy_schema = valid_manifest();
1381        legacy_schema["schema"] = json!("supercov-rust-manifest-candidate-v1");
1382        assert!(matches!(
1383            RustCompilerManifest::parse(&serde_json::to_vec(&legacy_schema).unwrap()),
1384            Err(RustCompilerManifestError::Invalid(_))
1385        ));
1386        let mut previous_schema = valid_manifest();
1387        previous_schema["schema"] = json!("supercov-rust-manifest-candidate-v2");
1388        assert!(matches!(
1389            RustCompilerManifest::parse(&serde_json::to_vec(&previous_schema).unwrap()),
1390            Err(RustCompilerManifestError::Invalid(_))
1391        ));
1392
1393        let mut missing_alternative_canonical = valid_manifest();
1394        missing_alternative_canonical["branches"][0]["alternatives"][0]
1395            .as_object_mut()
1396            .unwrap()
1397            .remove("canonical");
1398        assert!(matches!(
1399            RustCompilerManifest::parse(
1400                &serde_json::to_vec(&missing_alternative_canonical).unwrap()
1401            ),
1402            Err(RustCompilerManifestError::Json(_))
1403        ));
1404
1405        let mut unknown = valid_manifest();
1406        unknown["unexpected"] = json!(true);
1407        assert!(matches!(
1408            RustCompilerManifest::parse(&serde_json::to_vec(&unknown).unwrap()),
1409            Err(RustCompilerManifestError::Json(_))
1410        ));
1411
1412        let mut collision = valid_manifest();
1413        collision["decisions"][0]["probeOrdinal"] = json!("1");
1414        assert!(matches!(
1415            RustCompilerManifest::parse(&serde_json::to_vec(&collision).unwrap()),
1416            Err(RustCompilerManifestError::Invalid(_))
1417        ));
1418
1419        let mut complete = valid_manifest();
1420        complete["measurementComplete"] = json!(true);
1421        assert!(matches!(
1422            RustCompilerManifest::parse(&serde_json::to_vec(&complete).unwrap()),
1423            Err(RustCompilerManifestError::Invalid(_))
1424        ));
1425
1426        let mut traversal = valid_manifest();
1427        traversal["points"][0]["sourceKey"] = json!("source:../outside.rs");
1428        assert!(matches!(
1429            RustCompilerManifest::parse(&serde_json::to_vec(&traversal).unwrap()),
1430            Err(RustCompilerManifestError::Invalid(_))
1431        ));
1432
1433        let mut missing_outcome = valid_manifest();
1434        missing_outcome["decisions"][0]["outcomeBranchId"] = json!("rs:branch:missing");
1435        assert!(matches!(
1436            RustCompilerManifest::parse(&serde_json::to_vec(&missing_outcome).unwrap()),
1437            Err(RustCompilerManifestError::Invalid(_))
1438        ));
1439
1440        let mut wrong_outcome_kind = valid_manifest();
1441        wrong_outcome_kind["branches"][0]["kind"] = json!("loop-entry");
1442        wrong_outcome_kind["branches"][0]["alternatives"] = json!([
1443            {"id": "rs:branch-alternative:000000000000000000000003", "label": "zero iterations", "probeOrdinal": "3", "canonical": "zero"},
1444            {"id": "rs:branch-alternative:000000000000000000000004", "label": "entered", "probeOrdinal": "4", "canonical": "entered"}
1445        ]);
1446        assert!(matches!(
1447            RustCompilerManifest::parse(&serde_json::to_vec(&wrong_outcome_kind).unwrap()),
1448            Err(RustCompilerManifestError::Invalid(_))
1449        ));
1450
1451        let mut loop_manifest = valid_manifest();
1452        loop_manifest["decisions"][0]["kind"] = json!("while");
1453        loop_manifest["decisions"][0]["loopBranchId"] = json!("rs:branch:000000000000000000000006");
1454        loop_manifest["branches"].as_array_mut().unwrap().push(json!({
1455            "id": "rs:branch:000000000000000000000006",
1456            "kind": "loop-entry",
1457            "discriminator": "loop-entry:while",
1458            "sourceKey": "source:src/lib.rs",
1459            "start": 11,
1460            "end": 20,
1461            "provenance": "authored-source",
1462            "probeOrdinal": "6",
1463            "definitions": ["fixture::function"],
1464            "alternatives": [
1465                {"id": "rs:branch-alternative:000000000000000000000007", "label": "zero iterations", "probeOrdinal": "7", "canonical": "zero"},
1466                {"id": "rs:branch-alternative:000000000000000000000008", "label": "entered", "probeOrdinal": "8", "canonical": "entered"}
1467            ],
1468            "canonical": "loop-entry"
1469        }));
1470        let parsed_loop =
1471            RustCompilerManifest::parse(&serde_json::to_vec(&loop_manifest).unwrap()).unwrap();
1472        assert_eq!(
1473            parsed_loop.decisions[0].loop_branch_id.as_deref(),
1474            Some("rs:branch:000000000000000000000006")
1475        );
1476        let normalized_loop = parsed_loop
1477            .normalize(&BTreeMap::from([(
1478                "source:src/lib.rs".into(),
1479                RustCompilerSource {
1480                    file: "src/lib.rs".into(),
1481                    source: "0123456789 value and more source bytes".into(),
1482                },
1483            )]))
1484            .unwrap();
1485        assert_eq!(
1486            normalized_loop.decision_loop_obligations["rs:decision:000000000000000000000005"],
1487            (
1488                "rs:branch-alternative:000000000000000000000007".into(),
1489                "rs:branch-alternative:000000000000000000000008".into(),
1490            )
1491        );
1492
1493        let mut missing_loop = loop_manifest.clone();
1494        missing_loop["decisions"][0]["loopBranchId"] = json!(null);
1495        assert!(matches!(
1496            RustCompilerManifest::parse(&serde_json::to_vec(&missing_loop).unwrap()),
1497            Err(RustCompilerManifestError::Invalid(_))
1498        ));
1499
1500        let mut non_loop_relation = loop_manifest;
1501        non_loop_relation["decisions"][0]["kind"] = json!("if");
1502        assert!(matches!(
1503            RustCompilerManifest::parse(&serde_json::to_vec(&non_loop_relation).unwrap()),
1504            Err(RustCompilerManifestError::Invalid(_))
1505        ));
1506    }
1507
1508    #[test]
1509    fn logical_selections_are_strict_relations_projected_from_decision_vectors() {
1510        let mut value = valid_manifest();
1511        value["branches"].as_array_mut().unwrap().push(json!({
1512            "id": "rs:branch:000000000000000000000006",
1513            "kind": "logical-selection",
1514            "discriminator": "logical-selection:and",
1515            "sourceKey": "source:src/lib.rs",
1516            "start": 11,
1517            "end": 20,
1518            "provenance": "authored-source",
1519            "probeOrdinal": "6",
1520            "definitions": ["fixture::function"],
1521            "alternatives": [
1522                {"id": "rs:branch-alternative:000000000000000000000007", "label": "short-circuited", "probeOrdinal": "7", "canonical": "short"},
1523                {"id": "rs:branch-alternative:000000000000000000000008", "label": "right operand evaluated", "probeOrdinal": "8", "canonical": "evaluated"}
1524            ],
1525            "canonical": "logical-selection"
1526        }));
1527        value["decisions"][0]["conditions"] = json!([
1528            {"sourceKey": "source:src/lib.rs", "start": 11, "end": 15, "source": "left"},
1529            {"sourceKey": "source:src/lib.rs", "start": 16, "end": 20, "source": "right"}
1530        ]);
1531        value["decisions"][0]["logicalSelections"] = json!([{
1532            "branchId": "rs:branch:000000000000000000000006",
1533            "rightConditionIndex": 1
1534        }]);
1535
1536        let manifest = RustCompilerManifest::parse(&serde_json::to_vec(&value).unwrap()).unwrap();
1537        let normalized = manifest
1538            .normalize(&BTreeMap::from([(
1539                "source:src/lib.rs".into(),
1540                RustCompilerSource {
1541                    file: "src/lib.rs".into(),
1542                    source: "0123456789 left && right and more source bytes".into(),
1543                },
1544            )]))
1545            .unwrap();
1546        let relation = &normalized.decision_logical_selection_obligations["rs:decision:000000000000000000000005"]
1547            [0];
1548        assert_eq!(relation.right_condition_index, 1);
1549        assert_eq!(
1550            relation.short_circuited_id,
1551            "rs:branch-alternative:000000000000000000000007"
1552        );
1553        assert_eq!(
1554            relation.right_evaluated_id,
1555            "rs:branch-alternative:000000000000000000000008"
1556        );
1557        assert!(normalized.internal_ordinals.contains(&7));
1558        assert!(normalized.internal_ordinals.contains(&8));
1559        assert!(!normalized.hit_obligations_by_ordinal.contains_key(&7));
1560        assert!(!normalized.hit_obligations_by_ordinal.contains_key(&8));
1561
1562        let mut missing_relation = value.clone();
1563        missing_relation["decisions"][0]["logicalSelections"] = json!([]);
1564        let value_selection =
1565            RustCompilerManifest::parse(&serde_json::to_vec(&missing_relation).unwrap())
1566                .unwrap()
1567                .normalize(&BTreeMap::from([(
1568                    "source:src/lib.rs".into(),
1569                    RustCompilerSource {
1570                        file: "src/lib.rs".into(),
1571                        source: "0123456789 left && right and more source bytes".into(),
1572                    },
1573                )]))
1574                .unwrap();
1575        assert!(value_selection.hit_obligations_by_ordinal.contains_key(&7));
1576        assert!(value_selection.hit_obligations_by_ordinal.contains_key(&8));
1577
1578        let mut source_ordered = value.clone();
1579        source_ordered["branches"].as_array_mut().unwrap().push(json!({
1580            "id": "rs:branch:000000000000000000000009",
1581            "kind": "logical-selection",
1582            "discriminator": "logical-selection:or",
1583            "sourceKey": "source:src/lib.rs",
1584            "start": 11,
1585            "end": 20,
1586            "provenance": "authored-source",
1587            "probeOrdinal": "9",
1588            "definitions": ["fixture::function"],
1589            "alternatives": [
1590                {"id": "rs:branch-alternative:000000000000000000000010", "label": "short-circuited", "probeOrdinal": "10", "canonical": "short-two"},
1591                {"id": "rs:branch-alternative:000000000000000000000011", "label": "right operand evaluated", "probeOrdinal": "11", "canonical": "evaluated-two"}
1592            ],
1593            "canonical": "logical-selection-two"
1594        }));
1595        source_ordered["decisions"][0]["conditions"] = json!([
1596            {"sourceKey": "source:src/lib.rs", "start": 11, "end": 12, "source": "first"},
1597            {"sourceKey": "source:src/lib.rs", "start": 13, "end": 14, "source": "second"},
1598            {"sourceKey": "source:src/lib.rs", "start": 15, "end": 20, "source": "third"}
1599        ]);
1600        source_ordered["decisions"][0]["logicalSelections"] = json!([
1601            {"branchId": "rs:branch:000000000000000000000009", "rightConditionIndex": 1},
1602            {"branchId": "rs:branch:000000000000000000000006", "rightConditionIndex": 2}
1603        ]);
1604        let parsed_source_ordered =
1605            RustCompilerManifest::parse(&serde_json::to_vec(&source_ordered).unwrap()).unwrap();
1606        assert_eq!(
1607            parsed_source_ordered.decisions[0]
1608                .logical_selections
1609                .iter()
1610                .map(|selection| selection.right_condition_index)
1611                .collect::<Vec<_>>(),
1612            vec![1, 2],
1613        );
1614        source_ordered["decisions"][0]["logicalSelections"]
1615            .as_array_mut()
1616            .unwrap()
1617            .reverse();
1618        assert!(matches!(
1619            RustCompilerManifest::parse(&serde_json::to_vec(&source_ordered).unwrap()),
1620            Err(RustCompilerManifestError::Invalid(_))
1621        ));
1622
1623        let mut invalid_index = value;
1624        invalid_index["decisions"][0]["logicalSelections"][0]["rightConditionIndex"] = json!(2);
1625        assert!(matches!(
1626            RustCompilerManifest::parse(&serde_json::to_vec(&invalid_index).unwrap()),
1627            Err(RustCompilerManifestError::Invalid(_))
1628        ));
1629        let mut invalid_discriminator = valid_manifest();
1630        invalid_discriminator["branches"].as_array_mut().unwrap().push(json!({
1631            "id": "rs:branch:000000000000000000000006",
1632            "kind": "logical-selection",
1633            "discriminator": "logical-selection:unknown",
1634            "sourceKey": "source:src/lib.rs",
1635            "start": 11,
1636            "end": 20,
1637            "provenance": "authored-source",
1638            "probeOrdinal": "6",
1639            "definitions": ["fixture::function"],
1640            "alternatives": [
1641                {"id": "rs:branch-alternative:000000000000000000000007", "label": "short-circuited", "probeOrdinal": "7", "canonical": "short"},
1642                {"id": "rs:branch-alternative:000000000000000000000008", "label": "right operand evaluated", "probeOrdinal": "8", "canonical": "evaluated"}
1643            ],
1644            "canonical": "logical-selection"
1645        }));
1646        assert!(matches!(
1647            RustCompilerManifest::parse(&serde_json::to_vec(&invalid_discriminator).unwrap()),
1648            Err(RustCompilerManifestError::Invalid(_))
1649        ));
1650    }
1651
1652    #[test]
1653    fn accepts_exact_doctest_source_provenance() {
1654        let mut value = valid_manifest();
1655        value["points"][0]["provenance"] = json!("doctest-source");
1656
1657        let manifest = RustCompilerManifest::parse(&serde_json::to_vec(&value).unwrap()).unwrap();
1658        assert_eq!(manifest.points[0].provenance, "doctest-source");
1659    }
1660
1661    #[test]
1662    fn source_snapshot_envelope_is_strict_and_never_resolves_paths() {
1663        let snapshots = json!({
1664            "schema": SOURCE_SNAPSHOT_SCHEMA,
1665            "crate": "fixture",
1666            "sources": {
1667                "source:src/lib.rs": {"file": "src/lib.rs", "source": "fn work() {}\n"},
1668                "generated:package:.:generated.rs": {
1669                    "file": "generated:package:.:generated.rs",
1670                    "source": "fn generated() {}\n"
1671                }
1672            }
1673        });
1674        let parsed =
1675            RustCompilerSourceSnapshots::parse(&serde_json::to_vec(&snapshots).unwrap()).unwrap();
1676        assert_eq!(parsed.crate_name, "fixture");
1677        assert_eq!(parsed.sources.len(), 2);
1678
1679        let mut unknown = snapshots.clone();
1680        unknown["path"] = json!("/tmp/guess");
1681        assert!(
1682            RustCompilerSourceSnapshots::parse(&serde_json::to_vec(&unknown).unwrap()).is_err()
1683        );
1684
1685        let mut traversal = snapshots;
1686        traversal["sources"]["source:../outside.rs"] =
1687            json!({"file": "outside.rs", "source": "fn hidden() {}"});
1688        assert!(
1689            RustCompilerSourceSnapshots::parse(&serde_json::to_vec(&traversal).unwrap()).is_err()
1690        );
1691    }
1692
1693    #[test]
1694    fn repeated_compiler_units_merge_only_when_identity_and_source_are_exact() {
1695        let first =
1696            RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1697        let mut second = first.clone();
1698        second.points[0].definitions = vec![
1699            "fixture::function".into(),
1700            "fixture::tests::function".into(),
1701        ];
1702        let snapshots = RustCompilerSourceSnapshots::parse(
1703            &serde_json::to_vec(&json!({
1704                "schema": SOURCE_SNAPSHOT_SCHEMA,
1705                "crate": "fixture",
1706                "sources": {
1707                    "source:src/lib.rs": {
1708                        "file": "src/lib.rs",
1709                        "source": "0123456789 value and more source bytes"
1710                    }
1711                }
1712            }))
1713            .unwrap(),
1714        )
1715        .unwrap();
1716        let normalized = normalize_rust_compiler_candidates(vec![
1717            (first.clone(), snapshots.clone()),
1718            (second, snapshots.clone()),
1719        ])
1720        .unwrap();
1721        assert_eq!(normalized.manifest.points.len(), 1);
1722        assert_eq!(normalized.manifest.branches.len(), 1);
1723        assert_eq!(normalized.manifest.decisions.len(), 1);
1724
1725        let mut changed = snapshots;
1726        changed.sources.get_mut("source:src/lib.rs").unwrap().source =
1727            "changed source bytes that remain long enough".into();
1728        assert!(normalize_rust_compiler_candidates(vec![
1729            (first.clone(), RustCompilerSourceSnapshots::parse(
1730                &serde_json::to_vec(&json!({
1731                    "schema": SOURCE_SNAPSHOT_SCHEMA,
1732                    "crate": "fixture",
1733                    "sources": {"source:src/lib.rs": {"file": "src/lib.rs", "source": "0123456789 value and more source bytes"}}
1734                })).unwrap(),
1735            ).unwrap()),
1736            (first, changed),
1737        ]).is_err());
1738    }
1739
1740    #[test]
1741    fn normalizes_exact_source_locations_and_runtime_ordinals() {
1742        let manifest =
1743            RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1744        let sources = BTreeMap::from([(
1745            "source:src/lib.rs".into(),
1746            RustCompilerSource {
1747                file: "src/lib.rs".into(),
1748                source: "0123456789\nvalue && more text".into(),
1749            },
1750        )]);
1751        let normalized = manifest.normalize(&sources).unwrap();
1752        assert_eq!(normalized.manifest.points[0].source, "0123456789");
1753        assert_eq!(normalized.manifest.branches[0].line, 2);
1754        assert_eq!(normalized.manifest.branches[0].column, 0);
1755        assert_eq!(normalized.manifest.branches[0].source, "value && ");
1756        assert_eq!(
1757            normalized.hit_obligations_by_ordinal[&1],
1758            ["rs:function:000000000000000000000001"]
1759        );
1760        assert_eq!(
1761            normalized.hit_obligations_by_ordinal[&3],
1762            ["rs:branch-alternative:000000000000000000000003"]
1763        );
1764        assert_eq!(normalized.internal_ordinals, BTreeSet::from([2, 5]));
1765        assert_eq!(
1766            normalized.decision_outcome_obligations["rs:decision:000000000000000000000005"],
1767            (
1768                "rs:branch-alternative:000000000000000000000004".into(),
1769                "rs:branch-alternative:000000000000000000000003".into(),
1770            )
1771        );
1772        assert_eq!(normalized.manifest.limitations.len(), 1);
1773        let fingerprint = &normalized.manifest.scope.as_ref().unwrap()["sourceFingerprint"];
1774        assert_eq!(fingerprint["algorithm"], "sha256");
1775        assert_eq!(fingerprint["digest"].as_str().unwrap().len(), 64);
1776        assert_eq!(fingerprint["files"], 1);
1777        assert_eq!(fingerprint["generatedFiles"], 0);
1778    }
1779
1780    #[test]
1781    fn source_fingerprint_binds_full_generated_bytes_outside_obligation_ranges() {
1782        let mut candidate = valid_manifest();
1783        let generated_key = "generated:package:.:generated.rs";
1784        candidate["points"][0]["sourceKey"] = json!(generated_key);
1785        candidate["branches"][0]["sourceKey"] = json!(generated_key);
1786        candidate["decisions"][0]["sourceKey"] = json!(generated_key);
1787        candidate["decisions"][0]["conditions"][0]["sourceKey"] = json!(generated_key);
1788        let manifest =
1789            RustCompilerManifest::parse(&serde_json::to_vec(&candidate).unwrap()).unwrap();
1790        let baseline_sources = BTreeMap::from([(
1791            generated_key.into(),
1792            RustCompilerSource {
1793                file: generated_key.into(),
1794                source: "0123456789 value and more source bytes // baseline".into(),
1795            },
1796        )]);
1797        let changed_sources = BTreeMap::from([(
1798            generated_key.into(),
1799            RustCompilerSource {
1800                file: generated_key.into(),
1801                source: "0123456789 value and more source bytes // changed!".into(),
1802            },
1803        )]);
1804        let baseline = manifest.normalize(&baseline_sources).unwrap().manifest;
1805        let changed = manifest.normalize(&changed_sources).unwrap().manifest;
1806        assert_eq!(
1807            baseline.scope.as_ref().unwrap()["sourceFingerprint"]["generatedFiles"],
1808            1,
1809        );
1810        assert_ne!(
1811            baseline.scope.as_ref().unwrap()["sourceFingerprint"]["digest"],
1812            changed.scope.as_ref().unwrap()["sourceFingerprint"]["digest"],
1813        );
1814        let mut baseline_without_fingerprint = serde_json::to_value(&baseline).unwrap();
1815        let mut changed_without_fingerprint = serde_json::to_value(&changed).unwrap();
1816        baseline_without_fingerprint["scope"]
1817            .as_object_mut()
1818            .unwrap()
1819            .remove("sourceFingerprint");
1820        changed_without_fingerprint["scope"]
1821            .as_object_mut()
1822            .unwrap()
1823            .remove("sourceFingerprint");
1824        assert_eq!(baseline_without_fingerprint, changed_without_fingerprint);
1825    }
1826
1827    #[test]
1828    fn match_selection_expands_to_sibling_not_selected_obligations() {
1829        let mut candidate = valid_manifest();
1830        let outcome_branch = candidate["branches"][0].clone();
1831        candidate["branches"] = json!([
1832            outcome_branch,
1833            {
1834                "id": "rs:branch:000000000000000000000006",
1835                "kind": "match-arm",
1836                "discriminator": "match-arm:0",
1837                "sourceKey": "source:src/lib.rs",
1838                "start": 11,
1839                "end": 16,
1840                "provenance": "authored-source",
1841                "probeOrdinal": "6",
1842                "definitions": ["fixture::function"],
1843                "alternatives": [
1844                    {"id": "rs:branch-alternative:000000000000000000000007", "label": "not selected", "probeOrdinal": "7", "canonical": "not selected"},
1845                    {"id": "rs:branch-alternative:000000000000000000000008", "label": "selected", "probeOrdinal": "8", "canonical": "selected"}
1846                ],
1847                "canonical": "first arm"
1848            },
1849            {
1850                "id": "rs:branch:000000000000000000000009",
1851                "kind": "match-arm",
1852                "discriminator": "match-arm:1",
1853                "sourceKey": "source:src/lib.rs",
1854                "start": 17,
1855                "end": 22,
1856                "provenance": "authored-source",
1857                "probeOrdinal": "9",
1858                "definitions": ["fixture::function"],
1859                "alternatives": [
1860                    {"id": "rs:branch-alternative:00000000000000000000000a", "label": "not selected", "probeOrdinal": "10", "canonical": "not selected"},
1861                    {"id": "rs:branch-alternative:00000000000000000000000b", "label": "selected", "probeOrdinal": "11", "canonical": "selected"}
1862                ],
1863                "canonical": "second arm"
1864            }
1865        ]);
1866        candidate["selectionGroups"] = json!([{
1867            "id": "rs:match-group:00000000000000000000000c",
1868            "kind": "match",
1869            "sourceKey": "source:src/lib.rs",
1870            "start": 11,
1871            "end": 22,
1872            "provenance": "authored-source",
1873            "probeOrdinal": "12",
1874            "definitions": ["fixture::function"],
1875            "parentGroupId": null,
1876            "parentSite": null,
1877            "parentArmIndex": null,
1878            "arms": [
1879                {"branchId": "rs:branch:000000000000000000000006", "bodySourceKey": "source:src/lib.rs", "bodyStart": 11, "bodyEnd": 16, "guarded": false, "guardDecisionId": null, "selectedOrdinal": "8", "notSelectedOrdinal": "7"},
1880                {"branchId": "rs:branch:000000000000000000000009", "bodySourceKey": "source:src/lib.rs", "bodyStart": 17, "bodyEnd": 22, "guarded": false, "guardDecisionId": null, "selectedOrdinal": "11", "notSelectedOrdinal": "10"}
1881            ],
1882            "canonical": "match"
1883        }]);
1884        let manifest =
1885            RustCompilerManifest::parse(&serde_json::to_vec(&candidate).unwrap()).unwrap();
1886        let sources = BTreeMap::from([(
1887            "source:src/lib.rs".into(),
1888            RustCompilerSource {
1889                file: "src/lib.rs".into(),
1890                source: "0123456789\nfirst second trailing".into(),
1891            },
1892        )]);
1893        let normalized = manifest.normalize(&sources).unwrap();
1894        assert_eq!(
1895            normalized.hit_obligations_by_ordinal[&8],
1896            [
1897                "rs:branch-alternative:000000000000000000000008",
1898                "rs:branch-alternative:00000000000000000000000a",
1899            ]
1900        );
1901        assert_eq!(
1902            normalized.hit_obligations_by_ordinal[&11],
1903            [
1904                "rs:branch-alternative:000000000000000000000007",
1905                "rs:branch-alternative:00000000000000000000000b",
1906            ]
1907        );
1908    }
1909
1910    #[test]
1911    fn normalization_fails_closed_on_missing_or_non_utf8_boundary_sources() {
1912        let manifest =
1913            RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1914        assert!(matches!(
1915            manifest.normalize(&BTreeMap::new()),
1916            Err(RustCompilerManifestError::InvalidSource(_))
1917        ));
1918
1919        let mut candidate = valid_manifest();
1920        candidate["points"][0]["start"] = json!(1);
1921        candidate["points"][0]["end"] = json!(2);
1922        let manifest =
1923            RustCompilerManifest::parse(&serde_json::to_vec(&candidate).unwrap()).unwrap();
1924        let sources = BTreeMap::from([(
1925            "source:src/lib.rs".into(),
1926            RustCompilerSource {
1927                file: "src/lib.rs".into(),
1928                source: "é0123456789\nvalue && more text".into(),
1929            },
1930        )]);
1931        assert!(matches!(
1932            manifest.normalize(&sources),
1933            Err(RustCompilerManifestError::InvalidSource(_))
1934        ));
1935    }
1936}