devscout_rs/graph/compiler_facts/
admit.rs1use super::artifact::{parse_header, CandidateHeader, Coverage};
8use super::context_summary;
9use super::expectations::AdmissionExpectations;
10use super::reasons::RefusalReason;
11
12#[derive(Debug, Clone)]
15pub struct AdmittedFacts {
16 pub header: CandidateHeader,
18 pub bytes: Vec<u8>,
22}
23
24fn inventory_is_coherent(header: &CandidateHeader) -> bool {
28 !header
29 .units_processed
30 .iter()
31 .any(|unit| header.units_missing.contains(unit))
32}
33
34fn context_summary_matches(header: &CandidateHeader) -> bool {
41 match (
42 &header.context_fingerprint_header,
43 &header.context_compilations,
44 ) {
45 (Some(header_fp), Some(compilations)) => {
46 *header_fp == context_summary::recompute(compilations)
47 }
48 _ => false,
49 }
50}
51
52fn occurrence_self_consistency(header: &CandidateHeader) -> Option<RefusalReason> {
59 if header.occurrence_sites.is_some() != header.occurrences_capability_provided {
60 return Some(RefusalReason::OccurrenceCapabilityMismatch);
61 }
62
63 let sites = header.occurrence_sites.as_ref()?;
64 let compilations = header.context_compilations.as_ref()?;
69
70 for site in sites {
71 match compilations.iter().find(|c| c.identity == site.identity) {
72 None => return Some(RefusalReason::OccurrenceCompilationUnknown),
73 Some(matched) if matched.fingerprint.is_none() => {
74 return Some(RefusalReason::OccurrenceCompilationUnsupported)
75 }
76 Some(_) => {}
77 }
78 }
79
80 None
81}
82
83fn identity_check(
84 header: &CandidateHeader,
85 expected: &AdmissionExpectations,
86) -> Option<RefusalReason> {
87 if header.contract_version != expected.contract_version {
88 return Some(RefusalReason::ContractVersionMismatch);
89 }
90 if header.producer_name != expected.producer_name {
91 return Some(RefusalReason::ProducerMismatch);
92 }
93 if header.engine_revision != expected.engine_revision {
94 return Some(RefusalReason::EngineRevisionMismatch);
95 }
96 if header.profile != expected.profile {
97 return Some(RefusalReason::ProfileMismatch);
98 }
99 if header.dependency_fingerprint != expected.dependency_fingerprint {
100 return Some(RefusalReason::DependencyFingerprintMismatch);
101 }
102 if header.context_schema_version != expected.context_schema_version {
103 return Some(RefusalReason::ContextEnvelopeVersionUnrecognised);
104 }
105 if !context_summary_matches(header) {
106 return Some(RefusalReason::ContextFingerprintMismatch);
107 }
108 if let Some(expected_head) = &expected.head_sha {
109 if header.source_head_sha.as_deref() != Some(expected_head.as_str()) {
110 return Some(RefusalReason::SourceSnapshotMismatch);
111 }
112 }
113 None
114}
115
116pub fn admit(
127 candidate_bytes: &[u8],
128 expected: &AdmissionExpectations,
129) -> Result<AdmittedFacts, RefusalReason> {
130 let header = parse_header(candidate_bytes)?;
131
132 if !header.completion_terminal {
133 return Err(RefusalReason::MissingCompletionRecord);
134 }
135 if !inventory_is_coherent(&header) {
136 return Err(RefusalReason::IncoherentInventory);
137 }
138 if let Some(reason) = occurrence_self_consistency(&header) {
139 return Err(reason);
140 }
141 if let Some(reason) = identity_check(&header, expected) {
142 return Err(reason);
143 }
144
145 Ok(AdmittedFacts {
146 header,
147 bytes: candidate_bytes.to_vec(),
148 })
149}
150
151impl AdmittedFacts {
152 pub fn coverage(&self) -> &Coverage {
154 &self.header.coverage
155 }
156}