dpp_calc/kernel/ruleset.rs
1//! Ruleset identity and validity period types used across all `dpp-calc` calculators.
2
3use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6/// Opaque machine-readable identifier for a regulatory ruleset.
7///
8/// Examples: `"repairability-heuristic-v1"`, `"battery-cfb"`. Always a
9/// compile-time literal in every concrete `Ruleset` impl in this crate — never
10/// constructed from external/deserialized input — hence `&'static str` rather
11/// than `String`: every ruleset's `id()`/`version()` becomes a plain `static`
12/// instead of `OnceLock`-wrapped lazy-init ceremony.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
14pub struct RulesetId(pub &'static str);
15
16/// Semver-shaped version for a ruleset, tracking parameter changes across
17/// delegated-act amendments.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19pub struct RulesetVersion(pub &'static str);
20
21/// Whether, and from when, a ruleset version is legally in force.
22///
23/// The two variants are not "now" and "later" — they are "we know the date" and
24/// "the date is not knowable yet". EU staged obligations are routinely written
25/// as *"from <date> **or** N months after the entry into force of
26/// <act>, **whichever is the latest**"* — see Regulation (EU) 2023/1542
27/// Art. 7(1)–(3), Art. 8(1) and Art. 10(2)–(3). Until that act enters into
28/// force the application date is genuinely unknown, and asserting one anyway
29/// (a far-future sentinel, say) states a fact the regulation does not.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub enum Effectivity {
32 /// In force from a known calendar date, until an optional end date.
33 InForce {
34 /// First day this ruleset version applies (inclusive).
35 from: NaiveDate,
36 /// Last day it applies (inclusive), or `None` if open-ended.
37 until: Option<NaiveDate>,
38 },
39 /// The instrument that will date this ruleset has not entered into force,
40 /// so no application date can be stated.
41 Pending {
42 /// The empowerment being waited on, e.g.
43 /// `"EU 2023/1542 Art. 10(5), first subparagraph"`.
44 empowerment: &'static str,
45 /// The Commission's own deadline to adopt, where the regulation sets
46 /// one. A deadline is **not** an application date — it can pass unmet,
47 /// and several already have.
48 adoption_deadline: Option<NaiveDate>,
49 },
50}
51
52impl Effectivity {
53 /// In force from `from`, open-ended.
54 #[must_use]
55 pub const fn open(from: NaiveDate) -> Self {
56 Self::InForce { from, until: None }
57 }
58
59 /// In force for a closed period.
60 #[must_use]
61 pub const fn closed(from: NaiveDate, until: NaiveDate) -> Self {
62 Self::InForce {
63 from,
64 until: Some(until),
65 }
66 }
67
68 /// Awaiting an instrument that has not entered into force.
69 #[must_use]
70 pub const fn pending(empowerment: &'static str, adoption_deadline: Option<NaiveDate>) -> Self {
71 Self::Pending {
72 empowerment,
73 adoption_deadline,
74 }
75 }
76
77 /// Whether this ruleset governs `date`.
78 ///
79 /// A [`Pending`](Self::Pending) ruleset governs no date at all — not even a
80 /// far-future one.
81 #[must_use]
82 pub fn is_active_on(&self, date: NaiveDate) -> bool {
83 match self {
84 Self::InForce { from, until } => date >= *from && until.is_none_or(|u| date <= u),
85 Self::Pending { .. } => false,
86 }
87 }
88
89 /// Error unless this ruleset governs `date`.
90 ///
91 /// Distinguishes three outcomes that must never be conflated: a ruleset that
92 /// is **not yet effective** on a known date, one that has **expired**, and
93 /// one whose date is **undetermined** because the instrument dating it has
94 /// not entered into force.
95 pub fn ensure_active_on(
96 &self,
97 id: &RulesetId,
98 date: NaiveDate,
99 ) -> Result<(), crate::error::CalcError> {
100 match self {
101 Self::Pending { empowerment, .. } => {
102 Err(crate::error::CalcError::RulesetUndetermined {
103 id: id.0.to_owned(),
104 empowerment: (*empowerment).to_owned(),
105 })
106 }
107 Self::InForce { from, until } => {
108 if date < *from {
109 return Err(crate::error::CalcError::RulesetNotYetEffective {
110 id: id.0.to_owned(),
111 from: from.to_string(),
112 });
113 }
114 if let Some(until) = until
115 && date > *until
116 {
117 return Err(crate::error::CalcError::RulesetExpired {
118 id: id.0.to_owned(),
119 until: until.to_string(),
120 });
121 }
122 Ok(())
123 }
124 }
125 }
126}
127
128/// Structured legal citation for a regulatory ruleset.
129///
130/// Embedded in every [`Ruleset`] implementation so the authoritative source can
131/// be located programmatically — without reading source comments or external docs.
132/// This is the primary audit anchor for notified bodies.
133#[derive(Debug, Clone, Serialize)]
134pub struct RegulatoryBasis {
135 /// EU regulation or directive number (e.g. `"EU 2023/1669"`).
136 pub regulation: &'static str,
137 /// Relevant article and/or annex (e.g. `"Annex II, Annex III"`).
138 pub article: &'static str,
139 /// Harmonised standard used for the methodology (e.g. `"EN 45554:2021"`).
140 pub standard: Option<&'static str>,
141 /// JRC or other technical study underpinning the parameters (e.g. `"JRC128649"`).
142 pub technical_study: Option<&'static str>,
143 /// EUR-Lex or Official Journal URL for the authoritative source text.
144 pub source_url: Option<&'static str>,
145 /// Base ID of the successor ruleset version, if this one has been superseded.
146 /// Set when the ruleset's `Effectivity::InForce.until` is populated.
147 pub superseded_by: Option<&'static str>,
148}
149
150/// Common interface for all regulatory rulesets embedded in this crate.
151///
152/// There is deliberately no today-relative convenience method. A ruleset is
153/// selected against the date the governing law attached to the product — see
154/// [`AssessmentClock`](crate::clock::AssessmentClock) — and offering a
155/// `…_today()` helper alongside would make the wrong answer the shorter one to
156/// write.
157pub trait Ruleset {
158 fn id(&self) -> &RulesetId;
159 fn version(&self) -> &RulesetVersion;
160 fn effectivity(&self) -> &Effectivity;
161 /// Structured citation for the EU regulation that mandates this calculation.
162 fn regulatory_basis(&self) -> &RegulatoryBasis;
163}