1use std::sync::Arc;
21
22use axioval_ir::{Evidence, ObjectId};
23
24use crate::services::reviewable_exact_evidence;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
28pub enum EnvelopeMembershipError {
29 #[error("envelope membership evidence must be exact and reviewable")]
31 InexactEvidence,
32 #[error("envelope membership is unavailable for the requested derivation")]
34 Unavailable,
35 #[error("requested envelope derivation is not supported by this source")]
37 UnsupportedDerivation,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[non_exhaustive]
43pub enum EnvelopeDerivation {
44 AllSpaces,
46 GrossAreaGroups,
48}
49
50impl EnvelopeDerivation {
51 pub fn as_str(self) -> &'static str {
52 match self {
53 EnvelopeDerivation::AllSpaces => "all-spaces",
54 EnvelopeDerivation::GrossAreaGroups => "gross-area-groups",
55 }
56 }
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct EnvelopeMembershipRequest {
67 derivation: EnvelopeDerivation,
68 bounding: Vec<ObjectId>,
69}
70
71impl EnvelopeMembershipRequest {
72 pub fn new(derivation: EnvelopeDerivation, mut bounding: Vec<ObjectId>) -> Self {
76 bounding.sort();
77 bounding.dedup();
78 Self {
79 derivation,
80 bounding,
81 }
82 }
83 pub fn derivation(&self) -> EnvelopeDerivation {
84 self.derivation
85 }
86 pub fn bounding(&self) -> &[ObjectId] {
88 &self.bounding
89 }
90}
91
92#[derive(Clone, Debug, PartialEq)]
94pub struct EnvelopeMembershipEvidence {
95 request: EnvelopeMembershipRequest,
96 declared: Vec<ObjectId>,
97 derived: Vec<ObjectId>,
98 on_envelope: Vec<ObjectId>,
99 undeclared: Vec<ObjectId>,
100 evaluated_objects: usize,
101 evidence: Evidence,
102}
103
104impl EnvelopeMembershipEvidence {
105 pub fn try_new(
108 request: EnvelopeMembershipRequest,
109 mut declared: Vec<ObjectId>,
110 mut derived: Vec<ObjectId>,
111 evaluated_objects: usize,
112 evidence: Evidence,
113 ) -> Result<Self, EnvelopeMembershipError> {
114 if !reviewable_exact_evidence(&evidence) {
115 return Err(EnvelopeMembershipError::InexactEvidence);
116 }
117 declared.sort();
118 declared.dedup();
119 derived.sort();
120 derived.dedup();
121 Ok(Self {
122 request,
123 declared,
124 on_envelope: derived.clone(),
125 derived,
126 undeclared: Vec::new(),
127 evaluated_objects,
128 evidence,
129 })
130 }
131
132 #[must_use]
139 pub fn with_undeclared(mut self, mut undeclared: Vec<ObjectId>) -> Self {
140 undeclared.sort();
141 undeclared.dedup();
142 self.declared = difference(&self.declared, &undeclared);
143 self.derived = difference(&self.derived, &undeclared);
144 self.undeclared = undeclared;
145 self
146 }
147
148 pub fn request(&self) -> &EnvelopeMembershipRequest {
149 &self.request
150 }
151 pub fn declared(&self) -> &[ObjectId] {
153 &self.declared
154 }
155 pub fn derived(&self) -> &[ObjectId] {
158 &self.derived
159 }
160 pub fn on_envelope(&self) -> &[ObjectId] {
164 &self.on_envelope
165 }
166 pub fn undeclared(&self) -> &[ObjectId] {
168 &self.undeclared
169 }
170 pub fn evaluated_objects(&self) -> usize {
172 self.evaluated_objects
173 }
174 pub fn evidence(&self) -> &Evidence {
175 &self.evidence
176 }
177
178 pub fn agrees(&self) -> bool {
180 self.declared == self.derived
181 }
182
183 pub fn declared_only(&self) -> Vec<ObjectId> {
185 difference(&self.declared, &self.derived)
186 }
187
188 pub fn derived_only(&self) -> Vec<ObjectId> {
190 difference(&self.derived, &self.declared)
191 }
192}
193
194fn difference(left: &[ObjectId], right: &[ObjectId]) -> Vec<ObjectId> {
196 let mut out = Vec::new();
197 let (mut i, mut j) = (0, 0);
198 while i < left.len() {
199 match right.get(j) {
200 Some(candidate) if candidate < &left[i] => j += 1,
201 Some(candidate) if candidate == &left[i] => {
202 i += 1;
203 j += 1;
204 }
205 _ => {
206 out.push(left[i].clone());
207 i += 1;
208 }
209 }
210 }
211 out
212}
213
214pub trait EnvelopeMembershipService: Send + Sync + 'static {
218 fn measure_envelope_membership(
219 &self,
220 request: &EnvelopeMembershipRequest,
221 ) -> Result<EnvelopeMembershipEvidence, EnvelopeMembershipError>;
222}
223
224#[derive(Clone)]
226pub struct EnvelopeMembershipServiceHandle(Arc<dyn EnvelopeMembershipService>);
227
228impl EnvelopeMembershipServiceHandle {
229 pub fn new(service: Arc<dyn EnvelopeMembershipService>) -> Self {
230 Self(service)
231 }
232 pub fn measure_envelope_membership(
233 &self,
234 request: &EnvelopeMembershipRequest,
235 ) -> Result<EnvelopeMembershipEvidence, EnvelopeMembershipError> {
236 self.0.measure_envelope_membership(request)
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use axioval_ir::SourceId;
244
245 fn source() -> SourceId {
246 SourceId::new("cad", "m").unwrap()
247 }
248 fn oid(local: &str) -> ObjectId {
249 ObjectId::new(source(), local).unwrap()
250 }
251 fn build(declared: &[&str], derived: &[&str]) -> EnvelopeMembershipEvidence {
252 EnvelopeMembershipEvidence::try_new(
253 EnvelopeMembershipRequest::new(EnvelopeDerivation::AllSpaces, vec![oid("s")]),
254 declared.iter().map(|s| oid(s)).collect(),
255 derived.iter().map(|s| oid(s)).collect(),
256 3,
257 Evidence::exact(source(), "envelope:all-spaces"),
258 )
259 .unwrap()
260 }
261
262 #[test]
265 fn agreement_ignores_order_and_duplicates() {
266 assert!(build(&["w2", "w1", "w2"], &["w1", "w2"]).agrees());
267 }
268
269 #[test]
270 fn differences_are_reported_in_both_directions() {
271 let measured = build(&["w1", "w2"], &["w2", "w3"]);
272 assert!(!measured.agrees());
273 assert_eq!(measured.declared_only(), vec![oid("w1")]);
274 assert_eq!(measured.derived_only(), vec![oid("w3")]);
275 }
276
277 #[test]
278 fn empty_sets_agree_and_have_no_differences() {
279 let measured = build(&[], &[]);
280 assert!(measured.agrees());
281 assert!(measured.declared_only().is_empty());
282 assert!(measured.derived_only().is_empty());
283 }
284
285 #[test]
288 fn nothing_declared_against_derived_walls_is_a_difference() {
289 let measured = build(&[], &["w1", "w2"]);
290 assert!(!measured.agrees());
291 assert_eq!(measured.derived_only(), vec![oid("w1"), oid("w2")]);
292 assert!(measured.declared_only().is_empty());
293 }
294
295 #[test]
297 fn undeclared_objects_leave_both_sets() {
298 let measured = build(&["w1"], &["w1", "w2"]).with_undeclared(vec![oid("w2")]);
299 assert!(measured.agrees());
300 assert_eq!(measured.undeclared(), &[oid("w2")]);
301 assert_eq!(measured.on_envelope(), &[oid("w1"), oid("w2")]);
303 let measured = build(&["w3"], &[]).with_undeclared(vec![oid("w3")]);
304 assert!(measured.agrees());
305 assert!(measured.declared().is_empty());
306 }
307
308 #[test]
310 fn bounding_objects_are_canonical() {
311 let request = EnvelopeMembershipRequest::new(
312 EnvelopeDerivation::AllSpaces,
313 vec![oid("s2"), oid("s1"), oid("s2")],
314 );
315 assert_eq!(request.bounding(), &[oid("s1"), oid("s2")]);
316 assert_eq!(
317 request,
318 EnvelopeMembershipRequest::new(
319 EnvelopeDerivation::AllSpaces,
320 vec![oid("s1"), oid("s2")]
321 )
322 );
323 }
324
325 #[test]
326 fn inexact_evidence_is_refused() {
327 let result = EnvelopeMembershipEvidence::try_new(
328 EnvelopeMembershipRequest::new(EnvelopeDerivation::AllSpaces, vec![oid("s")]),
329 Vec::new(),
330 Vec::new(),
331 0,
332 Evidence {
333 source: source(),
334 locator: "envelope:estimate".into(),
335 exact: false,
336 },
337 );
338 assert_eq!(result, Err(EnvelopeMembershipError::InexactEvidence));
339 }
340}