Skip to main content

rig_typesafeai/
questions.rs

1//! Typed definitions bind each question to its answer type.
2use crate::types::{Answer, Question};
3use rig_core::error::ProviderError;
4use rig_core::wasm_compat::{WasmCompatSend, WasmCompatSync};
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use serde_json::Value;
7use std::{collections::BTreeMap, marker::PhantomData};
8
9/// A selected application value and the full distribution over alternatives.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ChoiceAnswer<T: Ord> {
12    /// The selected application value.
13    pub choice: T,
14    /// The full distribution over the supplied alternatives or levels.
15    pub probabilities: BTreeMap<T, f64>,
16    /// Provider-reported concentration, not a probability of correctness.
17    pub confidence: f64,
18}
19/// Expected zero-based rubric position and the distribution that produced it.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DynamicScoreAnswer {
22    /// Probability-weighted mean of zero-based rubric indices.
23    pub score: f64,
24    /// The full distribution over the supplied alternatives or levels.
25    pub probabilities: BTreeMap<usize, f64>,
26    /// Level indices mapped to their original descriptions.
27    pub legend: BTreeMap<usize, Value>,
28    /// Provider-reported distribution concentration.
29    pub confidence: f64,
30}
31/// Probability of yes. Thresholds belong to the application's policy.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct NoulAnswer {
34    /// Probability that the answer is yes.
35    pub noul: f64,
36}
37
38/// A choice question whose successful answer contains `T`.
39#[derive(Debug)]
40pub struct Choice<T> {
41    instructions: Value,
42    criteria: BTreeMap<String, Option<Value>>,
43    marker: PhantomData<fn() -> T>,
44}
45
46/// A score question with a statically known ordered rubric.
47#[derive(Debug)]
48pub struct Score<L> {
49    instructions: Value,
50    criteria: Vec<Value>,
51    levels: Vec<L>,
52}
53
54/// A score question whose rubric is supplied at runtime.
55#[derive(Debug, Clone)]
56pub struct DynamicScore {
57    instructions: Value,
58    criteria: Vec<Value>,
59}
60
61/// A yes/no question whose successful answer is `NoulAnswer`.
62#[derive(Debug, Clone)]
63pub struct Noul {
64    instructions: Value,
65    criteria: Option<BTreeMap<String, Value>>,
66}
67
68impl<T> Clone for Choice<T> {
69    fn clone(&self) -> Self {
70        Self {
71            instructions: self.instructions.clone(),
72            criteria: self.criteria.clone(),
73            marker: PhantomData,
74        }
75    }
76}
77impl<L: Clone> Clone for Score<L> {
78    fn clone(&self) -> Self {
79        Self {
80            instructions: self.instructions.clone(),
81            criteria: self.criteria.clone(),
82            levels: self.levels.clone(),
83        }
84    }
85}
86
87/// Expected zero-based position and a distribution keyed by rubric variants.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ScoreAnswer<L: Ord> {
90    /// Probability-weighted mean of zero-based positions in the supplied rubric.
91    pub score: f64,
92    /// Probability for each rubric variant.
93    pub probabilities: BTreeMap<L, f64>,
94    /// Provider-reported distribution concentration.
95    pub confidence: f64,
96}
97
98impl<L: Ord> Score<L> {
99    /// Define an ordered, typed rubric. Array order determines zero-based wire
100    /// positions; the enum's discriminants and ordering do not.
101    ///
102    /// ```compile_fail,E0080
103    /// use rig_typesafeai::Score;
104    /// let _ = Score::new("Rate risk", [(0u8, "Only one level")]);
105    /// ```
106    pub fn new<const N: usize, D: Serialize>(
107        instructions: impl Serialize,
108        levels: [(L, D); N],
109    ) -> Result<Self, ProviderError> {
110        const { assert!(N >= 2 && N <= 10, "score requires 2 to 10 levels") };
111        Self::try_from_iter(instructions, levels)
112    }
113
114    /// Define a typed rubric from a runtime collection, validating its size and
115    /// rejecting repeated variants before evaluation.
116    pub fn try_from_iter<I, D>(
117        instructions: impl Serialize,
118        levels: I,
119    ) -> Result<Self, ProviderError>
120    where
121        I: IntoIterator<Item = (L, D)>,
122        D: Serialize,
123    {
124        let mut variants = Vec::new();
125        let mut criteria = Vec::new();
126        for (level, description) in levels {
127            if variants.contains(&level) {
128                return Err(ProviderError::request("score levels must be unique"));
129            }
130            variants.push(level);
131            criteria.push(content(description)?);
132        }
133        if !(2..=10).contains(&criteria.len()) {
134            return Err(ProviderError::request("score requires 2 to 10 levels"));
135        }
136        Ok(Self {
137            instructions: content(instructions)?,
138            criteria,
139            levels: variants,
140        })
141    }
142}
143
144impl<L: Ord + Clone + WasmCompatSend + WasmCompatSync> Query for Score<L> {
145    type Response = Answer;
146    type Output = ScoreAnswer<L>;
147    fn decode(&self, response: Answer) -> Result<Self::Output, ProviderError> {
148        let answer: DynamicScoreAnswer = decode_question(&self.definition(), &response)?;
149        let probabilities = answer
150            .probabilities
151            .into_iter()
152            .map(|(index, probability)| {
153                let level = self.levels.get(index).ok_or_else(|| {
154                    ProviderError::Response(format!("unknown score level: {index}"))
155                })?;
156                Ok((level.clone(), probability))
157            })
158            .collect::<Result<_, ProviderError>>()?;
159        Ok(ScoreAnswer {
160            score: answer.score,
161            confidence: answer.confidence,
162            probabilities,
163        })
164    }
165}
166impl<L> Score<L> {
167    fn definition(&self) -> Question {
168        Question::Score {
169            instructions: self.instructions.clone(),
170            criteria: self.criteria.clone(),
171        }
172    }
173}
174
175fn content(value: impl Serialize) -> Result<Value, ProviderError> {
176    let value = serde_json::to_value(value)?;
177    if !matches!(
178        value,
179        Value::String(_) | Value::Object(_) | Value::Array(_) | Value::Null
180    ) {
181        return Err(ProviderError::request(
182            "question content must be a string, object, array, or null",
183        ));
184    }
185    Ok(value)
186}
187fn question_id(id: impl Into<String>) -> Result<String, ProviderError> {
188    let id = id.into();
189    if id.is_empty() {
190        return Err(ProviderError::request("question ID cannot be empty"));
191    }
192    Ok(id)
193}
194impl<T: Serialize + DeserializeOwned + Ord> Choice<T> {
195    /// Define a fixed set of alternatives, with its count checked at compile time.
196    /// Serde string names become labels; descriptions accept strings, objects,
197    /// arrays, or `None` when the label needs no explanation.
198    ///
199    /// ```compile_fail,E0080
200    /// use rig_typesafeai::Choice;
201    /// let _ = Choice::new("Choose a route", [("only".to_owned(), "One option")]);
202    /// ```
203    pub fn new<const N: usize, D: Serialize>(
204        instructions: impl Serialize,
205        alternatives: [(T, D); N],
206    ) -> Result<Self, ProviderError> {
207        const { assert!(N >= 2 && N <= 255, "choice requires 2 to 255 alternatives") };
208        Self::try_from_iter(instructions, alternatives)
209    }
210
211    /// Define runtime-supplied alternatives, validating their count at construction.
212    pub fn try_from_iter<I, D>(
213        instructions: impl Serialize,
214        alternatives: I,
215    ) -> Result<Self, ProviderError>
216    where
217        I: IntoIterator<Item = (T, D)>,
218        D: Serialize,
219    {
220        let mut criteria = BTreeMap::new();
221        for (value, description) in alternatives {
222            let Value::String(label) = serde_json::to_value(&value)? else {
223                return Err(ProviderError::request(
224                    "choice values must serialize to strings",
225                ));
226            };
227            let decoded: T =
228                serde_json::from_value(Value::String(label.clone())).map_err(|_| {
229                    ProviderError::request(
230                        "choice labels must deserialize to their original values",
231                    )
232                })?;
233            if decoded != value {
234                return Err(ProviderError::request(
235                    "choice labels must round-trip without changing values",
236                ));
237            }
238            let description = serde_json::to_value(description)?;
239            let description = match description {
240                Value::Null => None,
241                description => Some(content(description)?),
242            };
243            if label.is_empty() || criteria.insert(label, description).is_some() {
244                return Err(ProviderError::request(
245                    "choice labels must be nonempty and unique",
246                ));
247            }
248        }
249        if !(2..=255).contains(&criteria.len()) {
250            return Err(ProviderError::request(
251                "choice requires 2 to 255 alternatives",
252            ));
253        }
254        Ok(Self {
255            instructions: content(instructions)?,
256            criteria,
257            marker: PhantomData,
258        })
259    }
260}
261impl DynamicScore {
262    /// Define a fixed rubric with two to ten levels, checked at compile time.
263    /// Levels run from low to high; descriptions may be structured.
264    ///
265    /// ```compile_fail,E0080
266    /// use rig_typesafeai::DynamicScore;
267    /// let _ = DynamicScore::new("Rate severity", ["Only one level"]);
268    /// ```
269    pub fn new<const N: usize, D: Serialize>(
270        instructions: impl Serialize,
271        levels: [D; N],
272    ) -> Result<Self, ProviderError> {
273        const { assert!(N >= 2 && N <= 10, "score requires 2 to 10 levels") };
274        Self::try_from_iter(instructions, levels)
275    }
276
277    /// Define a runtime-supplied rubric, validating its count at construction.
278    pub fn try_from_iter<I, D>(
279        instructions: impl Serialize,
280        levels: I,
281    ) -> Result<Self, ProviderError>
282    where
283        I: IntoIterator<Item = D>,
284        D: Serialize,
285    {
286        let criteria = levels
287            .into_iter()
288            .map(content)
289            .collect::<Result<Vec<_>, _>>()?;
290        if !(2..=10).contains(&criteria.len()) {
291            return Err(ProviderError::request("score requires 2 to 10 levels"));
292        }
293        Ok(Self {
294            instructions: content(instructions)?,
295            criteria,
296        })
297    }
298}
299impl Noul {
300    /// Define a statement or question for which a high probability means yes.
301    pub fn new(instructions: impl Serialize) -> Result<Self, ProviderError> {
302        Ok(Self {
303            instructions: content(instructions)?,
304            criteria: None,
305        })
306    }
307    /// Clarify outcomes using strings, objects, arrays, or null descriptions.
308    pub fn criteria(
309        mut self,
310        yes: impl Serialize,
311        no: impl Serialize,
312    ) -> Result<Self, ProviderError> {
313        self.criteria = Some(BTreeMap::from([
314            ("true".into(), content(yes)?),
315            ("false".into(), content(no)?),
316        ]));
317        Ok(self)
318    }
319}
320
321/// A serializable query with a statically determined response and output.
322/// Struct field names (including Serde renames) supply the wire question IDs.
323/// Serde handles encoding and response structure; `decode` validates each field
324/// against its original question and converts it to its application answer.
325///
326/// ```
327/// use rig_core::error::ProviderError;
328/// use rig_typesafeai::{Noul, NoulAnswer, Query};
329/// use serde::{Serialize, Deserialize};
330///
331/// // The fields are declared once, for both questions and answers.
332/// #[derive(Serialize, Deserialize)]
333/// struct Assessment<R, V> {
334///     ready: R,
335///     needs_review: V,
336/// }
337///
338/// impl<R: Query, V: Query> Query for Assessment<R, V> {
339///     type Response = Assessment<R::Response, V::Response>;
340///     type Output = Assessment<R::Output, V::Output>;
341///
342///     fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
343///         Ok(Assessment {
344///             ready: self.ready.decode(response.ready)?,
345///             needs_review: self.needs_review.decode(response.needs_review)?,
346///         })
347///     }
348/// }
349///
350/// let query = Assessment {
351///     ready: Noul::new("Is this ready to ship?")?,
352///     needs_review: Noul::new("Does this need human review?")?,
353/// };
354/// # Ok::<(), ProviderError>(())
355/// ```
356pub trait Query: Serialize + WasmCompatSend + WasmCompatSync {
357    /// The intermediate response shape deserialized by Serde.
358    type Response: DeserializeOwned;
359    /// The validated application answer shape.
360    type Output;
361    /// Validate and convert a deserialized response. Composite queries delegate
362    /// to their fields, keeping validation inside the evaluation error boundary.
363    fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError>;
364
365    /// Give a standalone question a runtime ID. Named struct fields do not need this.
366    fn named(self, id: impl Into<String>) -> Result<NamedQuery<Self>, ProviderError>
367    where
368        Self: Sized,
369    {
370        Ok(NamedQuery {
371            id: question_id(id)?,
372            question: self,
373        })
374    }
375
376    /// Flatten two object-shaped queries into one evaluation request.
377    /// Duplicate IDs are rejected before sending the request. Child queries must
378    /// serialize with stable field names: decoding uses those names to partition
379    /// the response before delegating to each child.
380    fn join<Q: Query>(self, other: Q) -> JoinedQuery<Self, Q>
381    where
382        Self: Sized,
383    {
384        JoinedQuery {
385            first: self,
386            second: other,
387        }
388    }
389
390    /// Project validated answers into an application value.
391    fn map<A, F>(self, map: F) -> MappedQuery<Self, F>
392    where
393        Self: Sized,
394        F: Fn(Self::Output) -> A + WasmCompatSend + WasmCompatSync,
395    {
396        MappedQuery {
397            questions: self,
398            map,
399        }
400    }
401}
402
403/// A standalone question serialized under a runtime ID.
404#[derive(Debug, Clone)]
405pub struct NamedQuery<Q> {
406    id: String,
407    question: Q,
408}
409impl<Q: Serialize> Serialize for NamedQuery<Q> {
410    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
411        use serde::ser::SerializeMap;
412        let mut map = serializer.serialize_map(Some(1))?;
413        map.serialize_entry(&self.id, &self.question)?;
414        map.end()
415    }
416}
417impl<Q: Query> Query for NamedQuery<Q> {
418    type Response = BTreeMap<String, Q::Response>;
419    type Output = Q::Output;
420    fn decode(&self, mut response: Self::Response) -> Result<Self::Output, ProviderError> {
421        if response.len() != 1 {
422            return Err(ProviderError::Response(
423                "response question IDs differ from request".into(),
424            ));
425        }
426        let answer = response
427            .remove(&self.id)
428            .ok_or_else(|| ProviderError::Response(format!("missing answer: {}", self.id)))?;
429        self.question.decode(answer)
430    }
431}
432
433/// Two object-shaped queries serialized into a single flat request.
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct JoinedQuery<A, B> {
436    #[serde(flatten)]
437    first: A,
438    #[serde(flatten)]
439    second: B,
440}
441impl<A: Query, B: Query> Query for JoinedQuery<A, B> {
442    type Response = Value;
443    type Output = (A::Output, B::Output);
444    fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
445        Ok((
446            self.first
447                .decode(select_response(&self.first, &response)?)?,
448            self.second
449                .decode(select_response(&self.second, &response)?)?,
450        ))
451    }
452}
453
454fn select_response<Q: Query>(query: &Q, response: &Value) -> Result<Q::Response, ProviderError> {
455    let definitions = serde_json::to_value(query)?;
456    let definitions = definitions
457        .as_object()
458        .ok_or_else(|| ProviderError::request("joined queries must serialize as objects"))?;
459    let response = response
460        .as_object()
461        .ok_or_else(|| ProviderError::Response("answers must be an object".into()))?;
462    let selected = definitions
463        .keys()
464        .map(|id| {
465            response
466                .get(id)
467                .cloned()
468                .map(|value| (id.clone(), value))
469                .ok_or_else(|| ProviderError::Response(format!("missing answer: {id}")))
470        })
471        .collect::<Result<serde_json::Map<_, _>, _>>()?;
472    Ok(serde_json::from_value(Value::Object(selected))?)
473}
474
475/// A serializable query with a typed projection of its validated answers.
476#[derive(Debug, Clone, Serialize)]
477#[serde(transparent)]
478pub struct MappedQuery<Q, F> {
479    questions: Q,
480    #[serde(skip)]
481    map: F,
482}
483impl<Q: Query, F, A> Query for MappedQuery<Q, F>
484where
485    F: Fn(Q::Output) -> A + WasmCompatSend + WasmCompatSync,
486{
487    type Response = Q::Response;
488    type Output = A;
489    fn decode(&self, response: Self::Response) -> Result<A, ProviderError> {
490        Ok((self.map)(self.questions.decode(response)?))
491    }
492}
493impl<Q: Query + ?Sized> Query for &Q {
494    type Response = Q::Response;
495    type Output = Q::Output;
496    fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
497        (**self).decode(response)
498    }
499}
500impl<Q: Query> Query for BTreeMap<String, Q> {
501    type Response = BTreeMap<String, Q::Response>;
502    type Output = BTreeMap<String, Q::Output>;
503    fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
504        if !self.keys().eq(response.keys()) {
505            return Err(ProviderError::Response(
506                "response question IDs differ from request".into(),
507            ));
508        }
509        self.iter()
510            .zip(response)
511            .map(|((_, question), (id, answer))| Ok((id, question.decode(answer)?)))
512            .collect()
513    }
514}
515
516fn decode_question<A: crate::decode::DecodeAnswer>(
517    question: &Question,
518    answer: &Answer,
519) -> Result<A, ProviderError> {
520    validate(question, answer)?;
521    A::decode(answer)
522}
523macro_rules! primitive {
524    ([$($generics:tt)*] $question:ty => $answer:ty, $kind:ident) => {
525        impl $($generics)* $question {
526            fn definition(&self) -> Question {
527                Question::$kind { instructions: self.instructions.clone(), criteria: self.criteria.clone() }
528            }
529        }
530        impl $($generics)* Query for $question {
531            type Response = Answer;
532            type Output = $answer;
533            fn decode(&self, response: Answer) -> Result<Self::Output, ProviderError> {
534                decode_question(&self.definition(), &response)
535            }
536        }
537    };
538}
539primitive!([<T: DeserializeOwned + Ord>] Choice<T> => ChoiceAnswer<T>, Choice);
540primitive!([] DynamicScore => DynamicScoreAnswer, Score);
541primitive!([] Noul => NoulAnswer, Noul);
542
543impl<T> Serialize for Choice<T> {
544    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
545        Question::Choice {
546            instructions: self.instructions.clone(),
547            criteria: self.criteria.clone(),
548        }
549        .serialize(serializer)
550    }
551}
552impl<L> Serialize for Score<L> {
553    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
554        self.definition().serialize(serializer)
555    }
556}
557impl Serialize for DynamicScore {
558    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
559        self.definition().serialize(serializer)
560    }
561}
562impl Serialize for Noul {
563    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
564        self.definition().serialize(serializer)
565    }
566}
567
568pub(crate) fn validate_definition(id: &str, question: &Question) -> Result<(), ProviderError> {
569    question_id(id)?;
570    match question {
571        Question::Choice {
572            instructions,
573            criteria,
574        } => {
575            content(instructions)?;
576            if !(2..=255).contains(&criteria.len()) || criteria.keys().any(String::is_empty) {
577                return Err(ProviderError::request(
578                    "choice requires 2 to 255 nonempty labels",
579                ));
580            }
581            for description in criteria.values().flatten() {
582                content(description)?;
583            }
584        }
585        Question::Score {
586            instructions,
587            criteria,
588        } => {
589            content(instructions)?;
590            if !(2..=10).contains(&criteria.len()) {
591                return Err(ProviderError::request("score requires 2 to 10 levels"));
592            }
593            for description in criteria {
594                content(description)?;
595            }
596        }
597        Question::Noul {
598            instructions,
599            criteria,
600        } => {
601            content(instructions)?;
602            if let Some(criteria) = criteria {
603                if criteria.len() != 2
604                    || !criteria.contains_key("true")
605                    || !criteria.contains_key("false")
606                {
607                    return Err(ProviderError::request(
608                        "Noul criteria require true and false descriptions",
609                    ));
610                }
611                for description in criteria.values() {
612                    content(description)?;
613                }
614            }
615        }
616    }
617    Ok(())
618}
619
620fn probability(value: f64) -> Result<(), ProviderError> {
621    if !value.is_finite() || !(0.0..=1.0).contains(&value) {
622        return Err(ProviderError::Response(
623            "probability or confidence is outside [0, 1]".into(),
624        ));
625    }
626    Ok(())
627}
628fn rounded_to_hundredths(probabilities: &BTreeMap<String, f64>) -> bool {
629    probabilities.values().all(|value| {
630        let cents = value * 100.0;
631        (cents - cents.round()).abs() <= 1e-9
632    })
633}
634pub(crate) fn distribution(probabilities: &BTreeMap<String, f64>) -> Result<(), ProviderError> {
635    for &weight in probabilities.values() {
636        probability(weight)?;
637    }
638    // Recorded Jev responses round probabilities to hundredths independently.
639    // Allow their accumulated rounding error, but cap the relaxation so a large
640    // choice set cannot make a substantially unnormalized distribution valid.
641    let rounded_to_cents = rounded_to_hundredths(probabilities);
642    let tolerance = if rounded_to_cents {
643        (probabilities.len() as f64 * 0.005).min(0.02) + 1e-12
644    } else {
645        1e-3
646    };
647    if (probabilities.values().sum::<f64>() - 1.0).abs() > tolerance {
648        return Err(ProviderError::Response(
649            "probabilities do not sum to one".into(),
650        ));
651    }
652    Ok(())
653}
654pub(crate) fn validate(question: &Question, answer: &Answer) -> Result<(), ProviderError> {
655    match (question, answer) {
656        (
657            Question::Choice { criteria, .. },
658            Answer::Choice {
659                choice,
660                probabilities,
661                confidence,
662            },
663        ) => {
664            if !criteria.contains_key(choice) || !criteria.keys().eq(probabilities.keys()) {
665                return Err(ProviderError::Response(
666                    "choice labels differ from the requested alternatives".into(),
667                ));
668            }
669            distribution(probabilities)?;
670            probability(*confidence)?;
671            let selected = probabilities.get(choice).copied().ok_or_else(|| {
672                ProviderError::Response("selected choice has no probability".into())
673            })?;
674            if probabilities.values().any(|p| *p > selected + 1e-6) {
675                return Err(ProviderError::Response(
676                    "selected choice is not a maximum".into(),
677                ));
678            }
679        }
680        (
681            Question::Score { criteria, .. },
682            Answer::Score {
683                score,
684                probabilities,
685                legend,
686                confidence,
687            },
688        ) => {
689            let expected = criteria
690                .iter()
691                .enumerate()
692                .map(|(i, v)| (i.to_string(), v.clone()))
693                .collect::<BTreeMap<_, _>>();
694            if &expected != legend || !expected.keys().eq(probabilities.keys()) {
695                return Err(ProviderError::Response(
696                    "score levels differ from the requested rubric".into(),
697                ));
698            }
699            distribution(probabilities)?;
700            probability(*confidence)?;
701            if !score.is_finite() || *score < 0.0 || *score > (criteria.len() - 1) as f64 {
702                return Err(ProviderError::Response(
703                    "score is outside the rubric".into(),
704                ));
705            }
706            let total = probabilities.values().sum::<f64>();
707            // Keys were checked against the zero-based rubric above (at most ten
708            // levels), so BTreeMap order is also numeric order here.
709            let mean = probabilities
710                .values()
711                .enumerate()
712                .map(|(index, weight)| index as f64 * weight)
713                .sum::<f64>()
714                / total;
715            // Independently rounded weights can move the normalized mean by
716            // sum(|index - mean| * 0.005), since the unrounded weights sum
717            // to one. The score itself is also
718            // rounded to hundredths in recorded responses.
719            let weight_error = if rounded_to_hundredths(probabilities) {
720                (0..criteria.len())
721                    .map(|index| (index as f64 - mean).abs() * 0.005)
722                    .sum::<f64>()
723            } else {
724                0.0
725            };
726            if (*score - mean).abs() > weight_error + 0.005 + 1e-12 {
727                return Err(ProviderError::Response(
728                    "score differs from the probability-weighted mean".into(),
729                ));
730            }
731        }
732        (Question::Noul { .. }, Answer::Noul { noul }) => probability(*noul)?,
733        _ => {
734            return Err(ProviderError::Response(
735                "answer kind differs from question kind".into(),
736            ));
737        }
738    }
739    Ok(())
740}
741
742pub(crate) fn state(value: impl Serialize) -> Result<Value, ProviderError> {
743    let value = content(value)?;
744    if value.is_null() {
745        return Err(ProviderError::request("state cannot be null"));
746    }
747    Ok(value)
748}