1use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::policy::{compose_policy_outcomes, PolicyContribution, PolicyDecision, PolicyOutcome};
12use crate::{CoreError, CoreResult};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
16#[serde(rename_all = "snake_case")]
17pub enum ProofState {
18 FullChainVerified,
20 Partial,
23 Broken,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
29#[serde(rename_all = "snake_case")]
30pub enum ProofEdgeKind {
31 HashChain,
33 Signature,
35 IdentityRotation,
37 ExternalAnchor,
39 LineageClosure,
41 AuthorityFold,
43 ContextPackLink,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
49pub struct ProofEdge {
50 pub kind: ProofEdgeKind,
52 pub from_ref: String,
54 pub to_ref: String,
56 pub evidence_ref: Option<String>,
59}
60
61impl ProofEdge {
62 #[must_use]
64 pub fn new(
65 kind: ProofEdgeKind,
66 from_ref: impl Into<String>,
67 to_ref: impl Into<String>,
68 ) -> Self {
69 Self {
70 kind,
71 from_ref: from_ref.into(),
72 to_ref: to_ref.into(),
73 evidence_ref: None,
74 }
75 }
76
77 #[must_use]
79 pub fn with_evidence_ref(mut self, evidence_ref: impl Into<String>) -> Self {
80 self.evidence_ref = Some(evidence_ref.into());
81 self
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
87#[serde(rename_all = "snake_case")]
88pub enum ProofEdgeFailure {
89 Missing,
91 Unresolved,
93 Mismatch,
95 InvalidSignature,
97 AnchorMismatch,
99 AuthorityMismatch,
102}
103
104impl ProofEdgeFailure {
105 #[must_use]
108 pub const fn is_broken(self) -> bool {
109 match self {
110 Self::Missing | Self::Unresolved => false,
111 Self::Mismatch
112 | Self::InvalidSignature
113 | Self::AnchorMismatch
114 | Self::AuthorityMismatch => true,
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
121pub struct FailingEdge {
122 pub kind: ProofEdgeKind,
124 pub from_ref: String,
126 pub to_ref: Option<String>,
128 pub failure: ProofEdgeFailure,
130 pub reason: String,
133}
134
135impl FailingEdge {
136 #[must_use]
138 pub fn missing(
139 kind: ProofEdgeKind,
140 from_ref: impl Into<String>,
141 reason: impl Into<String>,
142 ) -> Self {
143 Self {
144 kind,
145 from_ref: from_ref.into(),
146 to_ref: None,
147 failure: ProofEdgeFailure::Missing,
148 reason: reason.into(),
149 }
150 }
151
152 #[must_use]
154 pub fn unresolved(
155 kind: ProofEdgeKind,
156 from_ref: impl Into<String>,
157 reason: impl Into<String>,
158 ) -> Self {
159 Self {
160 kind,
161 from_ref: from_ref.into(),
162 to_ref: None,
163 failure: ProofEdgeFailure::Unresolved,
164 reason: reason.into(),
165 }
166 }
167
168 #[must_use]
170 pub fn broken(
171 kind: ProofEdgeKind,
172 from_ref: impl Into<String>,
173 to_ref: impl Into<String>,
174 failure: ProofEdgeFailure,
175 reason: impl Into<String>,
176 ) -> Self {
177 debug_assert!(failure.is_broken());
178 let failure = if failure.is_broken() {
179 failure
180 } else {
181 ProofEdgeFailure::Mismatch
182 };
183
184 Self {
185 kind,
186 from_ref: from_ref.into(),
187 to_ref: Some(to_ref.into()),
188 failure,
189 reason: reason.into(),
190 }
191 }
192
193 #[must_use]
195 pub const fn is_broken(&self) -> bool {
196 self.failure.is_broken()
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
207pub struct ProofClosureReport {
208 #[serde(rename = "proof_state")]
209 state: ProofState,
210 verified_edges: Vec<ProofEdge>,
211 failing_edges: Vec<FailingEdge>,
212}
213
214impl ProofClosureReport {
215 #[must_use]
217 pub fn full_chain_verified(verified_edges: Vec<ProofEdge>) -> Self {
218 Self {
219 state: ProofState::FullChainVerified,
220 verified_edges,
221 failing_edges: Vec::new(),
222 }
223 }
224
225 #[must_use]
231 pub fn from_edges(verified_edges: Vec<ProofEdge>, failing_edges: Vec<FailingEdge>) -> Self {
232 let state = classify_failures(&failing_edges);
233 Self {
234 state,
235 verified_edges,
236 failing_edges,
237 }
238 }
239
240 #[must_use]
242 pub const fn state(&self) -> ProofState {
243 self.state
244 }
245
246 #[must_use]
248 pub fn verified_edges(&self) -> &[ProofEdge] {
249 &self.verified_edges
250 }
251
252 #[must_use]
254 pub fn failing_edges(&self) -> &[FailingEdge] {
255 &self.failing_edges
256 }
257
258 #[must_use]
260 pub const fn is_full_chain_verified(&self) -> bool {
261 matches!(self.state, ProofState::FullChainVerified)
262 }
263
264 #[must_use]
266 pub const fn is_broken(&self) -> bool {
267 matches!(self.state, ProofState::Broken)
268 }
269
270 pub fn push_failing_edge(&mut self, edge: FailingEdge) {
272 self.failing_edges.push(edge);
273 self.state = classify_failures(&self.failing_edges);
274 }
275
276 #[must_use]
278 pub fn with_failing_edge(mut self, edge: FailingEdge) -> Self {
279 self.push_failing_edge(edge);
280 self
281 }
282
283 #[must_use]
285 pub fn policy_decision(&self) -> PolicyDecision {
286 let outcome = match self.state {
287 ProofState::FullChainVerified => PolicyOutcome::Allow,
288 ProofState::Partial => PolicyOutcome::Quarantine,
289 ProofState::Broken => PolicyOutcome::Reject,
290 };
291 let reason = match self.state {
292 ProofState::FullChainVerified => "proof closure is fully verified",
293 ProofState::Partial => {
294 "proof closure is partial and cannot be treated as clean authority"
295 }
296 ProofState::Broken => "proof closure is broken and fails closed",
297 };
298 compose_policy_outcomes(
299 vec![
300 PolicyContribution::new("proof_closure.state", outcome, reason)
301 .expect("static policy contribution is valid"),
302 ],
303 None,
304 )
305 }
306
307 pub fn require_current_use_allowed(&self) -> CoreResult<()> {
309 let policy = self.policy_decision();
310 match policy.final_outcome {
311 PolicyOutcome::Reject | PolicyOutcome::Quarantine => {
312 Err(CoreError::Validation(format!(
313 "proof closure current use blocked by policy outcome {:?}",
314 policy.final_outcome
315 )))
316 }
317 PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => Ok(()),
318 }
319 }
320}
321
322const fn classify_failures(failing_edges: &[FailingEdge]) -> ProofState {
323 if failing_edges.is_empty() {
324 return ProofState::FullChainVerified;
325 }
326
327 let mut i = 0;
328 while i < failing_edges.len() {
329 if failing_edges[i].is_broken() {
330 return ProofState::Broken;
331 }
332 i += 1;
333 }
334
335 ProofState::Partial
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn edge() -> ProofEdge {
343 ProofEdge::new(ProofEdgeKind::HashChain, "evt_a", "evt_b").with_evidence_ref("hash_ab")
344 }
345
346 #[test]
347 fn full_report_cannot_carry_failures() {
348 let report = ProofClosureReport::full_chain_verified(vec![edge()]);
349 assert_eq!(report.state(), ProofState::FullChainVerified);
350 assert!(report.is_full_chain_verified());
351 assert!(report.failing_edges().is_empty());
352 }
353
354 #[test]
355 fn missing_edge_downgrades_to_partial() {
356 let report = ProofClosureReport::from_edges(
357 vec![edge()],
358 vec![FailingEdge::missing(
359 ProofEdgeKind::ExternalAnchor,
360 "tip_1",
361 "anchor not available",
362 )],
363 );
364
365 assert_eq!(report.state(), ProofState::Partial);
366 assert!(!report.is_full_chain_verified());
367 assert!(!report.is_broken());
368 }
369
370 #[test]
371 fn broken_edge_downgrades_to_broken() {
372 let report = ProofClosureReport::from_edges(
373 vec![edge()],
374 vec![FailingEdge::broken(
375 ProofEdgeKind::Signature,
376 "evt_a",
377 "sig_a",
378 ProofEdgeFailure::InvalidSignature,
379 "signature verification failed",
380 )],
381 );
382
383 assert_eq!(report.state(), ProofState::Broken);
384 assert!(report.is_broken());
385 }
386
387 #[test]
388 fn adding_failure_recomputes_state() {
389 let mut report = ProofClosureReport::full_chain_verified(vec![edge()]);
390 report.push_failing_edge(FailingEdge::unresolved(
391 ProofEdgeKind::LineageClosure,
392 "mem_a",
393 "source event not loaded",
394 ));
395
396 assert_eq!(report.state(), ProofState::Partial);
397 assert!(!report.is_full_chain_verified());
398 }
399
400 #[test]
401 fn proof_state_wire_strings_are_stable() {
402 assert_eq!(
403 serde_json::to_value(ProofState::FullChainVerified).unwrap(),
404 serde_json::json!("full_chain_verified")
405 );
406 assert_eq!(
407 serde_json::to_value(ProofState::Partial).unwrap(),
408 serde_json::json!("partial")
409 );
410 assert_eq!(
411 serde_json::to_value(ProofState::Broken).unwrap(),
412 serde_json::json!("broken")
413 );
414 }
415
416 #[test]
417 fn proof_report_serializes_proof_state_field() {
418 let report = ProofClosureReport::from_edges(
419 Vec::new(),
420 vec![FailingEdge::missing(
421 ProofEdgeKind::ExternalAnchor,
422 "tip_1",
423 "anchor not available",
424 )],
425 );
426 let serialized = serde_json::to_value(report).unwrap();
427
428 assert_eq!(serialized["proof_state"], serde_json::json!("partial"));
429 assert!(serialized.get("state").is_none());
430 }
431
432 #[test]
433 fn proof_state_derives_policy_decision() {
434 let full = ProofClosureReport::full_chain_verified(Vec::new());
435 let partial = ProofClosureReport::from_edges(
436 Vec::new(),
437 vec![FailingEdge::missing(
438 ProofEdgeKind::LineageClosure,
439 "memory:mem_01",
440 "source missing",
441 )],
442 );
443 let broken = ProofClosureReport::from_edges(
444 Vec::new(),
445 vec![FailingEdge::broken(
446 ProofEdgeKind::HashChain,
447 "event:a",
448 "event:b",
449 ProofEdgeFailure::Mismatch,
450 "hash mismatch",
451 )],
452 );
453
454 assert_eq!(full.policy_decision().final_outcome, PolicyOutcome::Allow);
455 assert_eq!(
456 partial.policy_decision().final_outcome,
457 PolicyOutcome::Quarantine
458 );
459 assert_eq!(
460 broken.policy_decision().final_outcome,
461 PolicyOutcome::Reject
462 );
463 }
464
465 #[test]
466 fn partial_or_broken_proof_fails_closed_for_current_use() {
467 let partial = ProofClosureReport::from_edges(
468 Vec::new(),
469 vec![FailingEdge::missing(
470 ProofEdgeKind::LineageClosure,
471 "memory:mem_01",
472 "source missing",
473 )],
474 );
475 let broken = ProofClosureReport::from_edges(
476 Vec::new(),
477 vec![FailingEdge::broken(
478 ProofEdgeKind::HashChain,
479 "event:a",
480 "event:b",
481 ProofEdgeFailure::Mismatch,
482 "hash mismatch",
483 )],
484 );
485
486 assert!(partial.require_current_use_allowed().is_err());
487 assert!(broken.require_current_use_allowed().is_err());
488 ProofClosureReport::full_chain_verified(Vec::new())
489 .require_current_use_allowed()
490 .expect("full chain proof supports current use");
491 }
492}