Skip to main content

sim_lib_music_serial/
invariant.rs

1//! Practice invariant ledgers and explicit relaxation evidence.
2
3use std::fmt::{Display, Formatter};
4
5/// Stable identity for one declared waiver.
6#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct WaiverId(String);
8
9/// Stable identity for one evidence item attached to a practice finding.
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct EvidenceId(String);
12
13fn validate_id(kind: &'static str, value: impl Into<String>) -> Result<String, String> {
14    let value = value.into();
15    if value.trim().is_empty() {
16        return Err(format!("{kind} cannot be empty"));
17    }
18    if value
19        .chars()
20        .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
21    {
22        return Err(format!(
23            "{kind} must use ASCII letters, digits, /, -, _, or ."
24        ));
25    }
26    Ok(value)
27}
28
29macro_rules! stable_id {
30    ($name:ident, $kind:literal, $doc:literal) => {
31        #[doc = $doc]
32        impl $name {
33            /// Creates a validated stable identifier.
34            pub fn new(value: impl Into<String>) -> Result<Self, String> {
35                Ok(Self(validate_id($kind, value)?))
36            }
37
38            /// Returns the stable text identity.
39            pub fn as_str(&self) -> &str {
40                &self.0
41            }
42        }
43
44        impl Display for $name {
45            fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
46                formatter.write_str(&self.0)
47            }
48        }
49    };
50}
51
52stable_id!(
53    WaiverId,
54    "waiver-id",
55    "Stable identity for one declared practice waiver."
56);
57stable_id!(
58    EvidenceId,
59    "evidence-id",
60    "Stable identity for one invariant-evidence record."
61);
62
63/// Status of one declared serial-practice invariant.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum InvariantStatus {
66    /// The expected fact held exactly under the selected reading.
67    Preserved,
68    /// The invariant would fail, but one explicit waiver declared the relaxation.
69    Relaxed {
70        /// Stable waiver id that authorized the relaxation.
71        waiver: WaiverId,
72    },
73    /// The invariant failed without a declared waiver.
74    Violated,
75    /// The selected reading did not expose evidence for this invariant.
76    NotApplicable,
77    /// The invariant could not be classified decisively.
78    Unknown,
79}
80
81/// One inspectable invariant result.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct InvariantLedgerEntry<R> {
84    /// Stable rule identity.
85    pub rule_id: R,
86    /// Stable invariant identity when the caller exposes one.
87    pub invariant_id: Option<String>,
88    /// Human-readable expected fact.
89    pub expected_fact: String,
90    /// Human-readable observed fact.
91    pub observed_fact: String,
92    /// Classified invariant status.
93    pub status: InvariantStatus,
94    /// Stable evidence items supporting the observation.
95    pub evidence_ids: Vec<EvidenceId>,
96    /// Explicit declared waiver, if any.
97    pub declared_waiver: Option<WaiverId>,
98}
99
100impl<R> InvariantLedgerEntry<R> {
101    pub(crate) fn new(
102        rule_id: R,
103        expected_fact: impl Into<String>,
104        observed_fact: impl Into<String>,
105        status: InvariantStatus,
106        evidence_ids: Vec<EvidenceId>,
107        declared_waiver: Option<WaiverId>,
108    ) -> Self {
109        Self {
110            rule_id,
111            invariant_id: None,
112            expected_fact: expected_fact.into(),
113            observed_fact: observed_fact.into(),
114            status,
115            evidence_ids,
116            declared_waiver,
117        }
118    }
119
120    pub(crate) fn with_invariant_id(mut self, invariant_id: impl Into<String>) -> Self {
121        self.invariant_id = Some(invariant_id.into());
122        self
123    }
124}
125
126/// Complete invariant bundle for one selected reading.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct InvariantLedger<R> {
129    entries: Vec<InvariantLedgerEntry<R>>,
130}
131
132impl<R> InvariantLedger<R> {
133    /// Creates one immutable invariant ledger.
134    pub fn new(entries: Vec<InvariantLedgerEntry<R>>) -> Self {
135        Self { entries }
136    }
137
138    /// Returns the recorded invariant entries in stable rule order.
139    pub fn entries(&self) -> &[InvariantLedgerEntry<R>] {
140        &self.entries
141    }
142
143    /// Returns `true` when the named invariant is present and preserved.
144    pub fn is_preserved(&self, invariant_id: &str) -> bool {
145        self.entries.iter().any(|entry| {
146            entry.invariant_id.as_deref() == Some(invariant_id)
147                && matches!(entry.status, InvariantStatus::Preserved)
148        })
149    }
150
151    /// Returns `true` when the named invariant is present and relaxed.
152    pub fn is_relaxed(&self, invariant_id: &str) -> bool {
153        self.entries.iter().any(|entry| {
154            entry.invariant_id.as_deref() == Some(invariant_id)
155                && matches!(entry.status, InvariantStatus::Relaxed { .. })
156        })
157    }
158}