Skip to main content

paramodel_elements/
value.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Parameter values, provenance, and canonical fingerprints.
5//!
6//! A `Value` is the observation of a `Parameter` at a particular point in
7//! a trial or binding. Each concrete variant owns the native Rust value
8//! plus a shared [`Provenance`] that records the owning parameter name,
9//! the instant the value was generated, an optional generator tag, and
10//! a BLAKE3 fingerprint over the canonical byte form.
11//!
12//! Canonical form (per SRD-0004 §Fingerprinting) is a tag byte, then the
13//! parameter name as UTF-8 with a `0x00` terminator, then the per-kind
14//! payload. The full byte layout is shipped with each variant's
15//! `fingerprint_of` helper so downstream tools can reproduce the hash
16//! without depending on this crate.
17//!
18//! Constructors always compute the fingerprint. `Value::verify_fingerprint`
19//! re-derives it and reports mismatches; callers run this at trust
20//! boundaries where tampering matters.
21
22use indexmap::IndexSet;
23use jiff::Timestamp;
24use serde::{Deserialize, Serialize};
25
26use crate::fingerprint::{Fingerprint, FingerprintBuilder};
27use crate::names::{NameError, ParameterName};
28
29// ---------------------------------------------------------------------------
30// Canonical tag bytes.
31// ---------------------------------------------------------------------------
32
33const TAG_INTEGER:   u8 = 0x01;
34const TAG_DOUBLE:    u8 = 0x02;
35const TAG_BOOLEAN:   u8 = 0x03;
36const TAG_STRING:    u8 = 0x04;
37const TAG_SELECTION: u8 = 0x05;
38
39/// Canonical quiet-NaN bit pattern.
40///
41/// Floats are hashed by their `to_le_bytes()`, which diverges for
42/// different NaN payloads. We fold every NaN input to this one pattern
43/// before hashing so `NaN` values fingerprint identically regardless of
44/// the arithmetic that produced them.
45const CANONICAL_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
46
47const fn canonicalise_f64(v: f64) -> f64 {
48    if v.is_nan() {
49        f64::from_bits(CANONICAL_NAN_BITS)
50    } else {
51        v
52    }
53}
54
55// ---------------------------------------------------------------------------
56// ValueKind discriminator.
57// ---------------------------------------------------------------------------
58
59/// Unit-only discriminator for a [`Value`] variant.
60///
61/// Useful when talking about "this is a Selection value" without
62/// borrowing the whole enum. Serialises as a lowercase tag
63/// (`"integer"`, `"double"`, `"boolean"`, `"string"`, `"selection"`).
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum ValueKind {
67    /// 64-bit signed integer.
68    Integer,
69    /// IEEE-754 `f64`.
70    Double,
71    /// Boolean.
72    Boolean,
73    /// UTF-8 string.
74    String,
75    /// Ordered multi-item selection from a registered domain.
76    Selection,
77}
78
79// ---------------------------------------------------------------------------
80// SelectionItem newtype.
81// ---------------------------------------------------------------------------
82
83/// A validated member of a selection domain.
84///
85/// Items are non-empty UTF-8 strings with no ASCII control characters.
86/// Constructor-side validation means a `SelectionItem` is always safe to
87/// hash into a canonical form.
88#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
89pub struct SelectionItem(String);
90
91impl SelectionItem {
92    /// Construct a new item, validating the candidate string.
93    pub fn new(candidate: impl Into<String>) -> Result<Self, NameError> {
94        let s = candidate.into();
95        if s.is_empty() {
96            return Err(NameError::Empty);
97        }
98        for (offset, ch) in s.char_indices() {
99            if ch.is_control() {
100                return Err(NameError::InvalidChar { ch, offset });
101            }
102        }
103        Ok(Self(s))
104    }
105
106    /// Borrow the inner string.
107    #[must_use]
108    pub fn as_str(&self) -> &str {
109        &self.0
110    }
111
112    /// Consume and return the inner string.
113    #[must_use]
114    pub fn into_inner(self) -> String {
115        self.0
116    }
117}
118
119impl std::fmt::Display for SelectionItem {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.write_str(&self.0)
122    }
123}
124
125impl std::fmt::Debug for SelectionItem {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "SelectionItem({:?})", self.0)
128    }
129}
130
131impl AsRef<str> for SelectionItem {
132    fn as_ref(&self) -> &str {
133        &self.0
134    }
135}
136
137impl Serialize for SelectionItem {
138    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
139        s.serialize_str(&self.0)
140    }
141}
142
143impl<'de> Deserialize<'de> for SelectionItem {
144    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
145        let s = String::deserialize(deserializer)?;
146        Self::new(s).map_err(serde::de::Error::custom)
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Generator provenance tags.
152// ---------------------------------------------------------------------------
153
154/// Which boundary a [`GeneratorInfo::Boundary`] value picked.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum BoundaryKind {
158    /// Numeric minimum.
159    Min,
160    /// Numeric maximum.
161    Max,
162    /// First item of an ordered set.
163    First,
164    /// Last item of an ordered set.
165    Last,
166}
167
168/// How a value was produced.
169///
170/// Parallels upstream's generator metadata. Stored inside [`Provenance`]
171/// as an option: a value constructed by a caller that doesn't care about
172/// the source leaves it `None`.
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(tag = "kind", rename_all = "snake_case")]
175pub enum GeneratorInfo {
176    /// Supplied verbatim by the caller.
177    Explicit,
178    /// Taken from the owning parameter's default.
179    Default,
180    /// Picked from the domain's boundary set.
181    Boundary {
182        /// Which boundary was selected.
183        which: BoundaryKind,
184    },
185    /// Drawn pseudo-randomly; seed is recorded when known.
186    Random {
187        /// RNG seed, if the caller tracked it.
188        seed: Option<u64>,
189    },
190    /// Computed from a derived parameter's expression.
191    Derived {
192        /// Source form of the derivation expression.
193        expression: String,
194    },
195}
196
197// ---------------------------------------------------------------------------
198// Provenance.
199// ---------------------------------------------------------------------------
200
201/// Shared metadata attached to every [`Value`].
202///
203/// Values of any kind carry the same provenance: the owning parameter,
204/// an observation timestamp, the optional generator tag, and the
205/// canonical fingerprint.
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct Provenance {
208    /// Name of the parameter this value observes.
209    pub parameter:    ParameterName,
210    /// When the value was constructed.
211    pub generated_at: Timestamp,
212    /// How the value was produced, if known.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub generator:    Option<GeneratorInfo>,
215    /// Canonical fingerprint of (kind, parameter, value bytes).
216    pub fingerprint:  Fingerprint,
217}
218
219// ---------------------------------------------------------------------------
220// Per-kind value structs.
221// ---------------------------------------------------------------------------
222
223/// An observed `i64` value.
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct IntegerValue {
226    /// The observed number.
227    pub value:      i64,
228    /// Shared provenance.
229    pub provenance: Provenance,
230}
231
232impl IntegerValue {
233    /// Construct, computing the fingerprint and stamping "now".
234    #[must_use]
235    pub fn new(name: ParameterName, value: i64, generator: Option<GeneratorInfo>) -> Self {
236        Self::new_at(name, value, generator, Timestamp::now())
237    }
238
239    /// Construct with an explicit timestamp (deterministic in tests).
240    #[must_use]
241    pub fn new_at(
242        name:      ParameterName,
243        value:     i64,
244        generator: Option<GeneratorInfo>,
245        now:       Timestamp,
246    ) -> Self {
247        let fingerprint = Self::fingerprint_of(&name, value);
248        Self {
249            value,
250            provenance: Provenance {
251                parameter: name,
252                generated_at: now,
253                generator,
254                fingerprint,
255            },
256        }
257    }
258
259    /// Canonical fingerprint for an integer value.
260    #[must_use]
261    pub fn fingerprint_of(name: &ParameterName, value: i64) -> Fingerprint {
262        FingerprintBuilder::new()
263            .byte(TAG_INTEGER)
264            .update(name.as_str().as_bytes())
265            .byte(0x00)
266            .i64_le(value)
267            .finish()
268    }
269}
270
271/// An observed `f64` value.
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
273pub struct DoubleValue {
274    /// The observed number. `NaN` is normalised before fingerprinting.
275    pub value:      f64,
276    /// Shared provenance.
277    pub provenance: Provenance,
278}
279
280impl DoubleValue {
281    /// Construct, computing the fingerprint and stamping "now".
282    #[must_use]
283    pub fn new(name: ParameterName, value: f64, generator: Option<GeneratorInfo>) -> Self {
284        Self::new_at(name, value, generator, Timestamp::now())
285    }
286
287    /// Construct with an explicit timestamp (deterministic in tests).
288    #[must_use]
289    pub fn new_at(
290        name:      ParameterName,
291        value:     f64,
292        generator: Option<GeneratorInfo>,
293        now:       Timestamp,
294    ) -> Self {
295        let fingerprint = Self::fingerprint_of(&name, value);
296        Self {
297            value: canonicalise_f64(value),
298            provenance: Provenance {
299                parameter: name,
300                generated_at: now,
301                generator,
302                fingerprint,
303            },
304        }
305    }
306
307    /// Canonical fingerprint for a double value. `NaN` collapses to the
308    /// canonical quiet-NaN pattern first.
309    #[must_use]
310    pub fn fingerprint_of(name: &ParameterName, value: f64) -> Fingerprint {
311        let canonical = canonicalise_f64(value);
312        FingerprintBuilder::new()
313            .byte(TAG_DOUBLE)
314            .update(name.as_str().as_bytes())
315            .byte(0x00)
316            .update(&canonical.to_le_bytes())
317            .finish()
318    }
319}
320
321/// An observed `bool` value.
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct BooleanValue {
324    /// The observed flag.
325    pub value:      bool,
326    /// Shared provenance.
327    pub provenance: Provenance,
328}
329
330impl BooleanValue {
331    /// Construct, computing the fingerprint and stamping "now".
332    #[must_use]
333    pub fn new(name: ParameterName, value: bool, generator: Option<GeneratorInfo>) -> Self {
334        Self::new_at(name, value, generator, Timestamp::now())
335    }
336
337    /// Construct with an explicit timestamp.
338    #[must_use]
339    pub fn new_at(
340        name:      ParameterName,
341        value:     bool,
342        generator: Option<GeneratorInfo>,
343        now:       Timestamp,
344    ) -> Self {
345        let fingerprint = Self::fingerprint_of(&name, value);
346        Self {
347            value,
348            provenance: Provenance {
349                parameter: name,
350                generated_at: now,
351                generator,
352                fingerprint,
353            },
354        }
355    }
356
357    /// Canonical fingerprint for a boolean value.
358    #[must_use]
359    pub fn fingerprint_of(name: &ParameterName, value: bool) -> Fingerprint {
360        FingerprintBuilder::new()
361            .byte(TAG_BOOLEAN)
362            .update(name.as_str().as_bytes())
363            .byte(0x00)
364            .byte(u8::from(value))
365            .finish()
366    }
367}
368
369/// An observed `String` value.
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371pub struct StringValue {
372    /// The observed text.
373    pub value:      String,
374    /// Shared provenance.
375    pub provenance: Provenance,
376}
377
378impl StringValue {
379    /// Construct, computing the fingerprint and stamping "now".
380    #[must_use]
381    pub fn new(name: ParameterName, value: impl Into<String>, generator: Option<GeneratorInfo>) -> Self {
382        Self::new_at(name, value, generator, Timestamp::now())
383    }
384
385    /// Construct with an explicit timestamp.
386    #[must_use]
387    pub fn new_at(
388        name:      ParameterName,
389        value:     impl Into<String>,
390        generator: Option<GeneratorInfo>,
391        now:       Timestamp,
392    ) -> Self {
393        let value = value.into();
394        let fingerprint = Self::fingerprint_of(&name, &value);
395        Self {
396            value,
397            provenance: Provenance {
398                parameter: name,
399                generated_at: now,
400                generator,
401                fingerprint,
402            },
403        }
404    }
405
406    /// Canonical fingerprint for a string value. Payload is a
407    /// `u32` LE length followed by the UTF-8 bytes.
408    #[must_use]
409    pub fn fingerprint_of(name: &ParameterName, value: &str) -> Fingerprint {
410        FingerprintBuilder::new()
411            .byte(TAG_STRING)
412            .update(name.as_str().as_bytes())
413            .byte(0x00)
414            .length_prefixed_str(value)
415            .finish()
416    }
417}
418
419/// An observed selection value.
420///
421/// Stored as an `IndexSet` so duplicates are impossible and authored
422/// order is preserved. Canonical form sorts items lexicographically
423/// before hashing so two selections that differ only in authored order
424/// fingerprint the same.
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
426pub struct SelectionValue {
427    /// The selected items, in authored order.
428    pub items:      IndexSet<SelectionItem>,
429    /// Shared provenance.
430    pub provenance: Provenance,
431}
432
433impl SelectionValue {
434    /// Construct, computing the fingerprint and stamping "now".
435    #[must_use]
436    pub fn new(
437        name:      ParameterName,
438        items:     IndexSet<SelectionItem>,
439        generator: Option<GeneratorInfo>,
440    ) -> Self {
441        Self::new_at(name, items, generator, Timestamp::now())
442    }
443
444    /// Construct with an explicit timestamp.
445    #[must_use]
446    pub fn new_at(
447        name:      ParameterName,
448        items:     IndexSet<SelectionItem>,
449        generator: Option<GeneratorInfo>,
450        now:       Timestamp,
451    ) -> Self {
452        let fingerprint = Self::fingerprint_of(&name, &items);
453        Self {
454            items,
455            provenance: Provenance {
456                parameter: name,
457                generated_at: now,
458                generator,
459                fingerprint,
460            },
461        }
462    }
463
464    /// Canonical fingerprint for a selection value. Items sort
465    /// lexicographically by UTF-8 bytes before hashing.
466    #[must_use]
467    pub fn fingerprint_of(name: &ParameterName, items: &IndexSet<SelectionItem>) -> Fingerprint {
468        let mut sorted: Vec<&str> = items.iter().map(SelectionItem::as_str).collect();
469        sorted.sort_unstable();
470        let len = u32::try_from(sorted.len()).expect("selection size fits in u32");
471        let mut builder = FingerprintBuilder::new()
472            .byte(TAG_SELECTION)
473            .update(name.as_str().as_bytes())
474            .byte(0x00)
475            .u32_le(len);
476        for item in sorted {
477            builder = builder.length_prefixed_str(item);
478        }
479        builder.finish()
480    }
481}
482
483// ---------------------------------------------------------------------------
484// The outer Value enum.
485// ---------------------------------------------------------------------------
486
487/// An observed parameter value, tagged by kind.
488///
489/// Serialises with a `kind` discriminator so wire formats are
490/// self-describing:
491///
492/// ```json
493/// { "kind": "integer", "value": 42, "provenance": { ... } }
494/// ```
495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496#[serde(tag = "kind", rename_all = "snake_case")]
497pub enum Value {
498    /// An `i64` observation.
499    Integer(IntegerValue),
500    /// An `f64` observation.
501    Double(DoubleValue),
502    /// A `bool` observation.
503    Boolean(BooleanValue),
504    /// A `String` observation.
505    String(StringValue),
506    /// A multi-item selection.
507    Selection(SelectionValue),
508}
509
510impl Value {
511    /// Discriminator for this value.
512    #[must_use]
513    pub const fn kind(&self) -> ValueKind {
514        match self {
515            Self::Integer(_)   => ValueKind::Integer,
516            Self::Double(_)    => ValueKind::Double,
517            Self::Boolean(_)   => ValueKind::Boolean,
518            Self::String(_)    => ValueKind::String,
519            Self::Selection(_) => ValueKind::Selection,
520        }
521    }
522
523    /// Shared provenance for this value.
524    #[must_use]
525    pub const fn provenance(&self) -> &Provenance {
526        match self {
527            Self::Integer(v)   => &v.provenance,
528            Self::Double(v)    => &v.provenance,
529            Self::Boolean(v)   => &v.provenance,
530            Self::String(v)    => &v.provenance,
531            Self::Selection(v) => &v.provenance,
532        }
533    }
534
535    /// Owning parameter.
536    #[must_use]
537    pub const fn parameter(&self) -> &ParameterName {
538        &self.provenance().parameter
539    }
540
541    /// Canonical fingerprint computed at construction.
542    #[must_use]
543    pub const fn fingerprint(&self) -> &Fingerprint {
544        &self.provenance().fingerprint
545    }
546
547    /// Borrow the `i64` payload, if this is an integer value.
548    #[must_use]
549    pub const fn as_integer(&self) -> Option<i64> {
550        if let Self::Integer(v) = self {
551            Some(v.value)
552        } else {
553            None
554        }
555    }
556
557    /// Borrow the `f64` payload, if this is a double value.
558    #[must_use]
559    pub const fn as_double(&self) -> Option<f64> {
560        if let Self::Double(v) = self {
561            Some(v.value)
562        } else {
563            None
564        }
565    }
566
567    /// Borrow the `bool` payload, if this is a boolean value.
568    #[must_use]
569    pub const fn as_boolean(&self) -> Option<bool> {
570        if let Self::Boolean(v) = self {
571            Some(v.value)
572        } else {
573            None
574        }
575    }
576
577    /// Borrow the `str` payload, if this is a string value.
578    #[must_use]
579    pub fn as_string(&self) -> Option<&str> {
580        if let Self::String(v) = self {
581            Some(&v.value)
582        } else {
583            None
584        }
585    }
586
587    /// Borrow the selection payload, if this is a selection value.
588    #[must_use]
589    pub const fn as_selection(&self) -> Option<&IndexSet<SelectionItem>> {
590        if let Self::Selection(v) = self {
591            Some(&v.items)
592        } else {
593            None
594        }
595    }
596
597    /// Convenience constructor for an integer value.
598    #[must_use]
599    pub fn integer(name: ParameterName, value: i64, generator: Option<GeneratorInfo>) -> Self {
600        Self::Integer(IntegerValue::new(name, value, generator))
601    }
602
603    /// Convenience constructor for a double value.
604    #[must_use]
605    pub fn double(name: ParameterName, value: f64, generator: Option<GeneratorInfo>) -> Self {
606        Self::Double(DoubleValue::new(name, value, generator))
607    }
608
609    /// Convenience constructor for a boolean value.
610    #[must_use]
611    pub fn boolean(name: ParameterName, value: bool, generator: Option<GeneratorInfo>) -> Self {
612        Self::Boolean(BooleanValue::new(name, value, generator))
613    }
614
615    /// Convenience constructor for a string value.
616    #[must_use]
617    pub fn string(name: ParameterName, value: impl Into<String>, generator: Option<GeneratorInfo>) -> Self {
618        Self::String(StringValue::new(name, value, generator))
619    }
620
621    /// Convenience constructor for a selection value.
622    #[must_use]
623    pub fn selection(
624        name:      ParameterName,
625        items:     IndexSet<SelectionItem>,
626        generator: Option<GeneratorInfo>,
627    ) -> Self {
628        Self::Selection(SelectionValue::new(name, items, generator))
629    }
630
631    /// Recompute the canonical fingerprint from the payload and compare
632    /// with the stored provenance fingerprint.
633    ///
634    /// Returns `true` when they match. Intended for tamper-detection
635    /// checkpoints; not run automatically during deserialisation.
636    #[must_use]
637    pub fn verify_fingerprint(&self) -> bool {
638        let expected = match self {
639            Self::Integer(v)   => IntegerValue::fingerprint_of(&v.provenance.parameter, v.value),
640            Self::Double(v)    => DoubleValue::fingerprint_of(&v.provenance.parameter, v.value),
641            Self::Boolean(v)   => BooleanValue::fingerprint_of(&v.provenance.parameter, v.value),
642            Self::String(v)    => StringValue::fingerprint_of(&v.provenance.parameter, &v.value),
643            Self::Selection(v) => SelectionValue::fingerprint_of(&v.provenance.parameter, &v.items),
644        };
645        &expected == self.fingerprint()
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652
653    fn pname(s: &str) -> ParameterName {
654        ParameterName::new(s).unwrap()
655    }
656
657    fn epoch() -> Timestamp {
658        Timestamp::from_second(0).unwrap()
659    }
660
661    // ---------- basic construction & accessors ----------
662
663    #[test]
664    fn integer_value_roundtrips_accessors() {
665        let v = Value::integer(pname("threads"), 42, None);
666        assert_eq!(v.kind(), ValueKind::Integer);
667        assert_eq!(v.as_integer(), Some(42));
668        assert_eq!(v.as_double(), None);
669        assert_eq!(v.parameter().as_str(), "threads");
670    }
671
672    #[test]
673    fn boolean_double_string_accessors() {
674        let b = Value::boolean(pname("on"), true, None);
675        let d = Value::double(pname("ratio"), 1.5, None);
676        let s = Value::string(pname("label"), "hi", None);
677        assert_eq!(b.as_boolean(), Some(true));
678        assert_eq!(d.as_double(), Some(1.5));
679        assert_eq!(s.as_string(), Some("hi"));
680    }
681
682    #[test]
683    fn selection_value_preserves_authored_order() {
684        let mut items = IndexSet::new();
685        items.insert(SelectionItem::new("gamma").unwrap());
686        items.insert(SelectionItem::new("alpha").unwrap());
687        items.insert(SelectionItem::new("beta").unwrap());
688        let v = Value::selection(pname("picks"), items, None);
689        let got: Vec<&str> = v.as_selection().unwrap().iter().map(SelectionItem::as_str).collect();
690        assert_eq!(got, vec!["gamma", "alpha", "beta"]);
691    }
692
693    // ---------- fingerprint canonical form ----------
694
695    #[test]
696    fn integer_fingerprint_is_deterministic() {
697        let a = IntegerValue::fingerprint_of(&pname("x"), 42);
698        let b = IntegerValue::fingerprint_of(&pname("x"), 42);
699        assert_eq!(a, b);
700    }
701
702    #[test]
703    fn integer_fingerprint_distinguishes_name_and_value() {
704        let base = IntegerValue::fingerprint_of(&pname("x"), 42);
705        assert_ne!(base, IntegerValue::fingerprint_of(&pname("y"), 42));
706        assert_ne!(base, IntegerValue::fingerprint_of(&pname("x"), 43));
707    }
708
709    #[test]
710    fn integer_fingerprint_matches_hand_built_bytes() {
711        // Reproduce the canonical form independently so a future
712        // refactor can't silently change the bytes we hash.
713        let name = pname("threads");
714        let got = IntegerValue::fingerprint_of(&name, 42);
715        let mut bytes = vec![TAG_INTEGER];
716        bytes.extend_from_slice(name.as_str().as_bytes());
717        bytes.push(0x00);
718        bytes.extend_from_slice(&42i64.to_le_bytes());
719        let expected = Fingerprint::of(&bytes);
720        assert_eq!(got, expected);
721    }
722
723    #[test]
724    fn double_nan_normalises() {
725        let nan_a = f64::NAN;
726        // Flip a payload bit to produce a different NaN bit pattern.
727        let nan_b = f64::from_bits(f64::NAN.to_bits() ^ 1);
728        assert!(nan_a.is_nan() && nan_b.is_nan());
729        assert_ne!(nan_a.to_bits(), nan_b.to_bits());
730
731        let fa = DoubleValue::fingerprint_of(&pname("r"), nan_a);
732        let fb = DoubleValue::fingerprint_of(&pname("r"), nan_b);
733        assert_eq!(fa, fb, "canonical NaN must collapse all payloads");
734    }
735
736    #[test]
737    fn double_value_stores_canonical_nan() {
738        let v = DoubleValue::new_at(
739            pname("r"),
740            f64::from_bits(f64::NAN.to_bits() ^ 1),
741            None,
742            epoch(),
743        );
744        assert_eq!(v.value.to_bits(), CANONICAL_NAN_BITS);
745    }
746
747    #[test]
748    fn boolean_fingerprint_distinguishes_true_and_false() {
749        let t = BooleanValue::fingerprint_of(&pname("b"), true);
750        let f = BooleanValue::fingerprint_of(&pname("b"), false);
751        assert_ne!(t, f);
752    }
753
754    #[test]
755    fn string_fingerprint_distinguishes_content() {
756        let a = StringValue::fingerprint_of(&pname("s"), "hello");
757        let b = StringValue::fingerprint_of(&pname("s"), "hellp");
758        assert_ne!(a, b);
759    }
760
761    #[test]
762    fn selection_fingerprint_is_order_independent() {
763        let mut one = IndexSet::new();
764        one.insert(SelectionItem::new("alpha").unwrap());
765        one.insert(SelectionItem::new("beta").unwrap());
766
767        let mut two = IndexSet::new();
768        two.insert(SelectionItem::new("beta").unwrap());
769        two.insert(SelectionItem::new("alpha").unwrap());
770
771        let fa = SelectionValue::fingerprint_of(&pname("s"), &one);
772        let fb = SelectionValue::fingerprint_of(&pname("s"), &two);
773        assert_eq!(fa, fb);
774    }
775
776    #[test]
777    fn selection_fingerprint_distinguishes_contents() {
778        let mut one = IndexSet::new();
779        one.insert(SelectionItem::new("alpha").unwrap());
780        let mut two = IndexSet::new();
781        two.insert(SelectionItem::new("beta").unwrap());
782        assert_ne!(
783            SelectionValue::fingerprint_of(&pname("s"), &one),
784            SelectionValue::fingerprint_of(&pname("s"), &two),
785        );
786    }
787
788    #[test]
789    fn kind_tags_are_disjoint() {
790        let name = pname("x");
791        let i = IntegerValue::fingerprint_of(&name, 0);
792        let d = DoubleValue::fingerprint_of(&name, 0.0);
793        let b = BooleanValue::fingerprint_of(&name, false);
794        let s = StringValue::fingerprint_of(&name, "");
795        let sel = SelectionValue::fingerprint_of(&name, &IndexSet::new());
796        let all = [i, d, b, s, sel];
797        for (ai, a) in all.iter().enumerate() {
798            for (bi, b) in all.iter().enumerate() {
799                if ai != bi {
800                    assert_ne!(a, b, "kinds {ai} and {bi} collided");
801                }
802            }
803        }
804    }
805
806    // ---------- verify_fingerprint ----------
807
808    #[test]
809    fn verify_fingerprint_passes_for_constructed_value() {
810        let v = Value::integer(pname("x"), 7, None);
811        assert!(v.verify_fingerprint());
812    }
813
814    #[test]
815    fn verify_fingerprint_detects_mutation() {
816        let mut iv = IntegerValue::new(pname("x"), 7, None);
817        iv.value = 8; // Tamper with the payload.
818        let v = Value::Integer(iv);
819        assert!(!v.verify_fingerprint());
820    }
821
822    // ---------- provenance / generator ----------
823
824    #[test]
825    fn generator_is_preserved() {
826        let v = Value::integer(
827            pname("x"),
828            7,
829            Some(GeneratorInfo::Random { seed: Some(42) }),
830        );
831        match v.provenance().generator.as_ref().unwrap() {
832            GeneratorInfo::Random { seed } => assert_eq!(*seed, Some(42)),
833            other => panic!("wrong generator: {other:?}"),
834        }
835    }
836
837    #[test]
838    fn new_at_uses_supplied_timestamp() {
839        let ts = Timestamp::from_second(1_700_000_000).unwrap();
840        let v = IntegerValue::new_at(pname("x"), 7, None, ts);
841        assert_eq!(v.provenance.generated_at, ts);
842    }
843
844    // ---------- serde ----------
845
846    #[test]
847    fn serde_roundtrip_integer_value() {
848        let ts = Timestamp::from_second(1_700_000_000).unwrap();
849        let v = Value::Integer(IntegerValue::new_at(pname("threads"), 42, None, ts));
850        let json = serde_json::to_string(&v).unwrap();
851        let back: Value = serde_json::from_str(&json).unwrap();
852        assert_eq!(v, back);
853        assert!(back.verify_fingerprint());
854    }
855
856    #[test]
857    fn serde_roundtrip_selection_value() {
858        let ts = Timestamp::from_second(0).unwrap();
859        let mut items = IndexSet::new();
860        items.insert(SelectionItem::new("alpha").unwrap());
861        items.insert(SelectionItem::new("beta").unwrap());
862        let v = Value::Selection(SelectionValue::new_at(pname("picks"), items, None, ts));
863        let json = serde_json::to_string(&v).unwrap();
864        let back: Value = serde_json::from_str(&json).unwrap();
865        assert_eq!(v, back);
866        assert!(back.verify_fingerprint());
867    }
868
869    #[test]
870    fn selection_item_rejects_empty_and_control_chars() {
871        assert!(SelectionItem::new("").is_err());
872        assert!(SelectionItem::new("hello\nworld").is_err());
873        assert!(SelectionItem::new("hello").is_ok());
874    }
875
876    #[test]
877    fn validation_kind_serialises_as_snake_case() {
878        let s = serde_json::to_string(&ValueKind::Selection).unwrap();
879        assert_eq!(s, "\"selection\"");
880    }
881}