1use super::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "kebab-case")]
10pub enum ProofUseRestrictionV1 {
11 MayPromote,
12 AdvisoryOnly,
13 QuarantineOnly,
14 BlockRelease,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
18pub struct ProofObligationV1 {
19 pub obligation_id: ArtifactId,
20 pub description: String,
21 pub required_evidence_family: String,
22 pub satisfied_by: Vec<ArtifactId>,
23 pub waived_by: Vec<ArtifactId>,
24}
25
26impl ProofObligationV1 {
27 pub fn new(
28 description: impl Into<String>,
29 required_evidence_family: impl Into<String>,
30 ) -> Self {
31 let description = description.into();
32 let required_evidence_family = required_evidence_family.into();
33 Self {
34 obligation_id: generated_artifact_id_from_material(
35 "proof-obligation",
36 &format!("{description}|{required_evidence_family}"),
37 ),
38 description,
39 required_evidence_family,
40 satisfied_by: Vec::new(),
41 waived_by: Vec::new(),
42 }
43 }
44
45 pub fn is_proven(&self) -> bool {
46 !self.satisfied_by.is_empty()
47 }
48
49 pub fn is_waived(&self) -> bool {
50 !self.waived_by.is_empty()
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
55pub struct LocalProofProfileV1 {
56 pub profile_id: ArtifactId,
57 pub proof_class: String,
58 pub exactness_tier: String,
59 pub obligations: Vec<ProofObligationV1>,
60 pub promotion_requires_all_obligations: bool,
61 pub waiver_policy: String,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub reason_codes: Vec<String>,
64}
65
66impl LocalProofProfileV1 {
67 pub fn local_exact(obligations: Vec<ProofObligationV1>) -> Self {
68 let material = obligations
69 .iter()
70 .map(|obligation| obligation.obligation_id.0.clone())
71 .collect::<Vec<_>>()
72 .join("|");
73 Self {
74 profile_id: generated_artifact_id_from_material("proof-profile", &material),
75 proof_class: "local-executable-evidence".into(),
76 exactness_tier: "exact-check-required".into(),
77 obligations,
78 promotion_requires_all_obligations: true,
79 waiver_policy: "waiver-is-governance-not-proof".into(),
80 reason_codes: vec!["proof-profile-declared".into()],
81 }
82 }
83
84 pub fn proof_satisfied(&self) -> bool {
85 self.obligations.iter().all(ProofObligationV1::is_proven)
86 }
87
88 pub fn has_waiver_without_proof(&self) -> bool {
89 self.obligations
90 .iter()
91 .any(|obligation| obligation.is_waived() && !obligation.is_proven())
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
96pub struct ProofDebtItemV1 {
97 pub debt_id: ArtifactId,
98 pub obligation_id: ArtifactId,
99 pub restriction: ProofUseRestrictionV1,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub allowed_uses: Vec<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub expires_at: Option<DateTime<Utc>>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub escalated_at: Option<DateTime<Utc>>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub escalation_reason: Option<String>,
108 pub reason: String,
109}
110
111impl ProofDebtItemV1 {
112 pub fn with_expiry(mut self, expires_at: DateTime<Utc>) -> Self {
113 self.expires_at = Some(expires_at);
114 self
115 }
116
117 pub fn escalated(mut self, escalated_at: DateTime<Utc>, reason: impl Into<String>) -> Self {
118 self.escalated_at = Some(escalated_at);
119 self.escalation_reason = Some(reason.into());
120 self.restriction = ProofUseRestrictionV1::BlockRelease;
121 self.allowed_uses = Vec::new();
122 self
123 }
124
125 pub fn is_expired_at(&self, now: DateTime<Utc>) -> bool {
126 self.expires_at
127 .map(|expires_at| now >= expires_at)
128 .unwrap_or(false)
129 }
130
131 pub fn is_escalated_at(&self, now: DateTime<Utc>) -> bool {
132 self.escalated_at
133 .map(|escalated_at| now >= escalated_at)
134 .unwrap_or(false)
135 || self.is_expired_at(now)
136 }
137
138 pub fn allows_use_at(&self, requested_use: &str, now: DateTime<Utc>) -> bool {
139 !self.is_escalated_at(now)
140 && matches!(self.restriction, ProofUseRestrictionV1::AdvisoryOnly)
141 && self
142 .allowed_uses
143 .iter()
144 .any(|allowed| allowed == requested_use)
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
149pub struct ProofDebtLedgerV1 {
150 pub ledger_id: ArtifactId,
151 pub artifact_ref: ArtifactId,
152 pub items: Vec<ProofDebtItemV1>,
153 pub generated_at: DateTime<Utc>,
154}
155
156impl ProofDebtLedgerV1 {
157 pub fn from_profile(artifact_ref: ArtifactId, profile: &LocalProofProfileV1) -> Self {
158 let items = profile
159 .obligations
160 .iter()
161 .filter(|obligation| !obligation.is_proven())
162 .map(|obligation| ProofDebtItemV1 {
163 debt_id: generated_artifact_id_from_material(
164 "proof-debt",
165 &format!("{}|{}", artifact_ref.0, obligation.obligation_id.0),
166 ),
167 obligation_id: obligation.obligation_id.clone(),
168 restriction: if obligation.is_waived() {
169 ProofUseRestrictionV1::AdvisoryOnly
170 } else {
171 ProofUseRestrictionV1::BlockRelease
172 },
173 allowed_uses: if obligation.is_waived() {
174 vec!["local-advisory-display".into(), "operator-triage".into()]
175 } else {
176 Vec::new()
177 },
178 expires_at: None,
179 escalated_at: None,
180 escalation_reason: None,
181 reason: if obligation.is_waived() {
182 "waiver-recorded-but-proof-missing".into()
183 } else {
184 "proof-obligation-unsatisfied".into()
185 },
186 })
187 .collect::<Vec<_>>();
188 let material = items
189 .iter()
190 .map(|item| item.debt_id.0.clone())
191 .collect::<Vec<_>>()
192 .join("|");
193 Self {
194 ledger_id: generated_artifact_id_from_material("proof-debt-ledger", &material),
195 artifact_ref,
196 items,
197 generated_at: Utc::now(),
198 }
199 }
200
201 pub fn blocks_promotion(&self) -> bool {
202 self.items.iter().any(|item| {
203 matches!(
204 item.restriction,
205 ProofUseRestrictionV1::BlockRelease
206 | ProofUseRestrictionV1::AdvisoryOnly
207 | ProofUseRestrictionV1::QuarantineOnly
208 )
209 })
210 }
211
212 pub fn active_items_at(&self, now: DateTime<Utc>) -> Vec<&ProofDebtItemV1> {
213 self.items
214 .iter()
215 .filter(|item| !item.is_expired_at(now))
216 .collect()
217 }
218
219 pub fn expired_items_at(&self, now: DateTime<Utc>) -> Vec<&ProofDebtItemV1> {
220 self.items
221 .iter()
222 .filter(|item| item.is_expired_at(now))
223 .collect()
224 }
225
226 pub fn escalated_items_at(&self, now: DateTime<Utc>) -> Vec<&ProofDebtItemV1> {
227 self.items
228 .iter()
229 .filter(|item| item.is_escalated_at(now))
230 .collect()
231 }
232
233 pub fn allows_use_at(&self, requested_use: &str, now: DateTime<Utc>) -> bool {
234 !self.items.is_empty()
235 && self
236 .items
237 .iter()
238 .all(|item| item.allows_use_at(requested_use, now))
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
243pub struct ProofWaiverReceiptV1 {
244 pub receipt_id: ArtifactId,
245 pub obligation_id: ArtifactId,
246 pub approver: String,
247 pub rationale: String,
248 pub waiver_is_not_proof: bool,
249 pub recorded_at: DateTime<Utc>,
250}
251
252impl ProofWaiverReceiptV1 {
253 pub fn new(
254 obligation_id: ArtifactId,
255 approver: impl Into<String>,
256 rationale: impl Into<String>,
257 ) -> Self {
258 let approver = approver.into();
259 let rationale = rationale.into();
260 Self {
261 receipt_id: generated_artifact_id_from_material(
262 "proof-waiver",
263 &format!("{}|{}|{}", obligation_id.0, approver, rationale),
264 ),
265 obligation_id,
266 approver,
267 rationale,
268 waiver_is_not_proof: true,
269 recorded_at: Utc::now(),
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
275pub struct PromotionEligibilityReportV1 {
276 pub report_id: ArtifactId,
277 pub artifact_ref: ArtifactId,
278 pub proof_profile_id: ArtifactId,
279 pub proof_debt_ledger_id: ArtifactId,
280 pub eligible: bool,
281 #[serde(default, skip_serializing_if = "Vec::is_empty")]
282 pub reason_codes: Vec<String>,
283 pub generated_at: DateTime<Utc>,
284}
285
286impl PromotionEligibilityReportV1 {
287 pub fn new(
288 artifact_ref: ArtifactId,
289 profile: &LocalProofProfileV1,
290 debt: &ProofDebtLedgerV1,
291 ) -> Self {
292 let eligible = profile.proof_satisfied() && !debt.blocks_promotion();
293 Self {
294 report_id: generated_artifact_id_from_material(
295 "promotion-eligibility",
296 &format!(
297 "{}|{}|{}",
298 artifact_ref.0, profile.profile_id.0, debt.ledger_id.0
299 ),
300 ),
301 artifact_ref,
302 proof_profile_id: profile.profile_id.clone(),
303 proof_debt_ledger_id: debt.ledger_id.clone(),
304 eligible,
305 reason_codes: if eligible {
306 vec!["promotion-proof-satisfied".into()]
307 } else {
308 vec!["promotion-blocked-by-proof-debt".into()]
309 },
310 generated_at: Utc::now(),
311 }
312 }
313}