molframe_core/contract/analysis.rs
1//! A result, and everything needed to judge it.
2//!
3//! An analysis returns a value *and* what it had to assume to produce one: how
4//! much of the intended data was actually there, which decisions were made on
5//! the caller's behalf, and where the inputs came from. The value alone is the
6//! part every library gives you; the rest is what makes it citable.
7//!
8//! Declining to answer is a result in its own right. A library that cannot
9//! return "no defensible number exists here" will always return a
10//! defensible-looking wrong one instead.
11
12use super::policy::{AnalysisPolicy, Fingerprint, PolicyField};
13use super::provenance::Provenance;
14use crate::diagnostic::Diagnostic;
15use std::ops::Deref;
16
17/// How far an analysis got.
18#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
19#[non_exhaustive]
20pub enum Status {
21 /// Every intended atom was available and unambiguous.
22 #[default]
23 Complete,
24 /// Computed, but some intended atoms were not available.
25 Partial,
26 /// Several defensible answers exist; one was chosen deterministically.
27 Ambiguous,
28 /// No defensible single answer exists under this policy.
29 ///
30 /// The value carried alongside this status is not meaningful.
31 Indeterminate,
32}
33
34impl Status {
35 /// Returns true when the value may be used as an answer.
36 #[must_use]
37 pub const fn is_usable(self) -> bool {
38 !matches!(self, Self::Indeterminate)
39 }
40}
41
42/// How much of what an analysis meant to use it actually used.
43///
44/// "Intended" comes from what a component should contain, not from what the
45/// file happened to have — so a residue whose side chain was never modelled
46/// reduces coverage rather than silently shrinking the calculation.
47#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
48pub struct Coverage {
49 /// Atoms the analysis meant to use.
50 pub intended: u32,
51 /// Atoms it did use.
52 pub used: u32,
53 /// Atoms absent from the model.
54 pub missing: u32,
55 /// Atoms present but not uniquely resolvable.
56 pub ambiguous: u32,
57}
58
59impl Coverage {
60 /// Coverage for an analysis that used everything it meant to.
61 #[must_use]
62 pub const fn complete(atoms: u32) -> Self {
63 Self {
64 intended: atoms,
65 used: atoms,
66 missing: 0,
67 ambiguous: 0,
68 }
69 }
70
71 /// The fraction of intended atoms that were used.
72 ///
73 /// An analysis over two fifths of the intended atoms is not the same
74 /// quantity as one over all of them, and this is what says so.
75 ///
76 /// # Panics
77 /// The checked conversion accepts every unsigned 32-bit count.
78 #[must_use]
79 pub fn fraction(self) -> f64 {
80 if self.intended == 0 {
81 1.0
82 } else {
83 f64::from(self.used) / f64::from(self.intended)
84 }
85 }
86
87 /// Returns true when nothing was missing or ambiguous.
88 #[must_use]
89 pub const fn is_complete(self) -> bool {
90 self.missing == 0 && self.ambiguous == 0 && self.used == self.intended
91 }
92}
93
94/// How much a decision could have changed the answer.
95#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
96#[non_exhaustive]
97pub enum ImpactEstimate {
98 /// The decision could not have changed anything here.
99 None,
100 /// Unlikely to matter.
101 Low,
102 /// Could matter.
103 Moderate,
104 /// Likely to matter.
105 High,
106 /// Not estimated.
107 #[default]
108 Unknown,
109}
110
111/// Where a decision came from.
112#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
113#[non_exhaustive]
114pub enum AssumptionSource {
115 /// The caller set it.
116 Explicit,
117 /// It came from the profile.
118 ProfileDefault,
119 /// It was inferred from the data.
120 Inferred,
121}
122
123/// One decision made on the caller's behalf.
124#[derive(Clone, PartialEq, Debug)]
125pub struct Assumption {
126 /// Which decision.
127 pub field: PolicyField,
128 /// What was decided.
129 pub value: Box<str>,
130 /// Where the decision came from.
131 pub source: AssumptionSource,
132 /// How much it could have mattered.
133 pub impact: ImpactEstimate,
134}
135
136impl Assumption {
137 /// Records a decision.
138 #[must_use]
139 pub fn new(
140 field: PolicyField,
141 value: impl Into<Box<str>>,
142 source: AssumptionSource,
143 impact: ImpactEstimate,
144 ) -> Self {
145 Self {
146 field,
147 value: value.into(),
148 source,
149 impact,
150 }
151 }
152
153 /// Returns true when the caller did not choose this and it could matter.
154 ///
155 /// These are the assumptions worth surfacing: a default the caller never saw
156 /// that changed the answer is the failure this whole layer exists to expose.
157 #[must_use]
158 pub fn is_silent_and_material(&self) -> bool {
159 self.source == AssumptionSource::ProfileDefault
160 && self.impact >= ImpactEstimate::Moderate
161 && self.impact != ImpactEstimate::Unknown
162 }
163}
164
165/// A value, and everything needed to judge it.
166///
167/// Dereferences to the value, so reading the answer costs one character and
168/// reading the rest is there when it matters. Ergonomics are load-bearing here:
169/// a contract that makes ordinary work painful gets bypassed, and a bypassed
170/// contract records nothing.
171///
172/// # Examples
173///
174/// ```
175/// use molframe_core::contract::{Analysis, AnalysisPolicy, Coverage, Status};
176///
177/// let policy = AnalysisPolicy::default();
178/// let radius = Analysis::complete(14.2_f64, Coverage::complete(1_960), &policy);
179///
180/// assert_eq!(*radius, 14.2);
181/// assert_eq!(radius.status, Status::Complete);
182/// assert_eq!(radius.coverage.fraction(), 1.0);
183/// ```
184#[derive(Clone, Debug)]
185pub struct Analysis<T> {
186 /// The answer.
187 pub value: T,
188 /// How far the analysis got.
189 pub status: Status,
190 /// How much of the intended data was used.
191 pub coverage: Coverage,
192 /// What the analysis found worth saying.
193 pub warnings: Vec<Diagnostic>,
194 /// What it decided on the caller's behalf.
195 pub assumptions: Vec<Assumption>,
196 /// Where the inputs came from and under what policy.
197 pub provenance: Provenance,
198}
199
200impl<T> Analysis<T> {
201 /// A complete result.
202 #[must_use]
203 pub fn complete(value: T, coverage: Coverage, policy: &AnalysisPolicy) -> Self {
204 Self {
205 value,
206 status: Status::Complete,
207 coverage,
208 warnings: Vec::new(),
209 assumptions: Vec::new(),
210 provenance: Provenance::new(policy),
211 }
212 }
213
214 /// A result computed over less than it intended.
215 #[must_use]
216 pub fn partial(value: T, coverage: Coverage, policy: &AnalysisPolicy) -> Self {
217 Self {
218 status: Status::Partial,
219 ..Self::complete(value, coverage, policy)
220 }
221 }
222
223 /// A refusal to answer.
224 ///
225 /// The value is whatever stands in for "nothing"; the status is the result.
226 #[must_use]
227 pub fn indeterminate(value: T, coverage: Coverage, policy: &AnalysisPolicy) -> Self {
228 Self {
229 status: Status::Indeterminate,
230 ..Self::complete(value, coverage, policy)
231 }
232 }
233
234 /// Attaches a finding.
235 #[must_use]
236 pub fn with_warning(mut self, warning: Diagnostic) -> Self {
237 self.warnings.push(warning);
238 self
239 }
240
241 /// Records a decision made on the caller's behalf.
242 #[must_use]
243 pub fn with_assumption(mut self, assumption: Assumption) -> Self {
244 self.assumptions.push(assumption);
245 self
246 }
247
248 /// The fingerprint of the policy this was computed under.
249 #[must_use]
250 pub fn policy_fingerprint(&self) -> Fingerprint {
251 self.provenance.policy_fingerprint
252 }
253
254 /// The decisions the caller never made that could have changed the answer.
255 pub fn silent_assumptions(&self) -> impl Iterator<Item = &Assumption> {
256 self.assumptions
257 .iter()
258 .filter(|assumption| assumption.is_silent_and_material())
259 }
260
261 /// Applies a function to the value, keeping everything else.
262 pub fn map<U>(self, transform: impl FnOnce(T) -> U) -> Analysis<U> {
263 Analysis {
264 value: transform(self.value),
265 status: self.status,
266 coverage: self.coverage,
267 warnings: self.warnings,
268 assumptions: self.assumptions,
269 provenance: self.provenance,
270 }
271 }
272}
273
274impl<T> Deref for Analysis<T> {
275 type Target = T;
276
277 fn deref(&self) -> &T {
278 &self.value
279 }
280}
281
282#[cfg(test)]
283#[path = "analysis_tests.rs"]
284mod tests;