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, Copy, Debug, PartialEq, Eq)]
61pub struct EnvelopeMembershipRequest {
62 derivation: EnvelopeDerivation,
63}
64
65impl EnvelopeMembershipRequest {
66 pub fn new(derivation: EnvelopeDerivation) -> Self {
67 Self { derivation }
68 }
69 pub fn derivation(&self) -> EnvelopeDerivation {
70 self.derivation
71 }
72}
73
74#[derive(Clone, Debug, PartialEq)]
76pub struct EnvelopeMembershipEvidence {
77 request: EnvelopeMembershipRequest,
78 declared: Vec<ObjectId>,
79 derived: Vec<ObjectId>,
80 evaluated_objects: usize,
81 evidence: Evidence,
82}
83
84impl EnvelopeMembershipEvidence {
85 pub fn try_new(
88 request: EnvelopeMembershipRequest,
89 mut declared: Vec<ObjectId>,
90 mut derived: Vec<ObjectId>,
91 evaluated_objects: usize,
92 evidence: Evidence,
93 ) -> Result<Self, EnvelopeMembershipError> {
94 if !reviewable_exact_evidence(&evidence) {
95 return Err(EnvelopeMembershipError::InexactEvidence);
96 }
97 declared.sort();
98 declared.dedup();
99 derived.sort();
100 derived.dedup();
101 Ok(Self {
102 request,
103 declared,
104 derived,
105 evaluated_objects,
106 evidence,
107 })
108 }
109
110 pub fn request(&self) -> EnvelopeMembershipRequest {
111 self.request
112 }
113 pub fn declared(&self) -> &[ObjectId] {
115 &self.declared
116 }
117 pub fn derived(&self) -> &[ObjectId] {
119 &self.derived
120 }
121 pub fn evaluated_objects(&self) -> usize {
123 self.evaluated_objects
124 }
125 pub fn evidence(&self) -> &Evidence {
126 &self.evidence
127 }
128
129 pub fn agrees(&self) -> bool {
131 self.declared == self.derived
132 }
133
134 pub fn declared_only(&self) -> Vec<ObjectId> {
136 difference(&self.declared, &self.derived)
137 }
138
139 pub fn derived_only(&self) -> Vec<ObjectId> {
141 difference(&self.derived, &self.declared)
142 }
143}
144
145fn difference(left: &[ObjectId], right: &[ObjectId]) -> Vec<ObjectId> {
147 let mut out = Vec::new();
148 let (mut i, mut j) = (0, 0);
149 while i < left.len() {
150 match right.get(j) {
151 Some(candidate) if candidate < &left[i] => j += 1,
152 Some(candidate) if candidate == &left[i] => {
153 i += 1;
154 j += 1;
155 }
156 _ => {
157 out.push(left[i].clone());
158 i += 1;
159 }
160 }
161 }
162 out
163}
164
165pub trait EnvelopeMembershipService: Send + Sync + 'static {
169 fn measure_envelope_membership(
170 &self,
171 request: &EnvelopeMembershipRequest,
172 ) -> Result<EnvelopeMembershipEvidence, EnvelopeMembershipError>;
173}
174
175#[derive(Clone)]
177pub struct EnvelopeMembershipServiceHandle(Arc<dyn EnvelopeMembershipService>);
178
179impl EnvelopeMembershipServiceHandle {
180 pub fn new(service: Arc<dyn EnvelopeMembershipService>) -> Self {
181 Self(service)
182 }
183 pub fn measure_envelope_membership(
184 &self,
185 request: &EnvelopeMembershipRequest,
186 ) -> Result<EnvelopeMembershipEvidence, EnvelopeMembershipError> {
187 self.0.measure_envelope_membership(request)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use axioval_ir::SourceId;
195
196 fn source() -> SourceId {
197 SourceId::new("cad", "m").unwrap()
198 }
199 fn oid(local: &str) -> ObjectId {
200 ObjectId::new(source(), local).unwrap()
201 }
202 fn build(declared: &[&str], derived: &[&str]) -> EnvelopeMembershipEvidence {
203 EnvelopeMembershipEvidence::try_new(
204 EnvelopeMembershipRequest::new(EnvelopeDerivation::AllSpaces),
205 declared.iter().map(|s| oid(s)).collect(),
206 derived.iter().map(|s| oid(s)).collect(),
207 3,
208 Evidence::exact(source(), "envelope:all-spaces"),
209 )
210 .unwrap()
211 }
212
213 #[test]
216 fn agreement_ignores_order_and_duplicates() {
217 assert!(build(&["w2", "w1", "w2"], &["w1", "w2"]).agrees());
218 }
219
220 #[test]
221 fn differences_are_reported_in_both_directions() {
222 let measured = build(&["w1", "w2"], &["w2", "w3"]);
223 assert!(!measured.agrees());
224 assert_eq!(measured.declared_only(), vec![oid("w1")]);
225 assert_eq!(measured.derived_only(), vec![oid("w3")]);
226 }
227
228 #[test]
229 fn empty_sets_agree_and_have_no_differences() {
230 let measured = build(&[], &[]);
231 assert!(measured.agrees());
232 assert!(measured.declared_only().is_empty());
233 assert!(measured.derived_only().is_empty());
234 }
235
236 #[test]
239 fn nothing_declared_against_derived_walls_is_a_difference() {
240 let measured = build(&[], &["w1", "w2"]);
241 assert!(!measured.agrees());
242 assert_eq!(measured.derived_only(), vec![oid("w1"), oid("w2")]);
243 assert!(measured.declared_only().is_empty());
244 }
245
246 #[test]
247 fn inexact_evidence_is_refused() {
248 let result = EnvelopeMembershipEvidence::try_new(
249 EnvelopeMembershipRequest::new(EnvelopeDerivation::AllSpaces),
250 Vec::new(),
251 Vec::new(),
252 0,
253 Evidence {
254 source: source(),
255 locator: "envelope:estimate".into(),
256 exact: false,
257 },
258 );
259 assert_eq!(result, Err(EnvelopeMembershipError::InexactEvidence));
260 }
261}