1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Core diffing logic for `affi receipt diff` (Micro-Task 1.2).
//!
//! This module provides the `DiffResult` structure and the algorithm to
//! compare two `Receipt` objects, identifying structural changes in the
//! event chain.
//!
//! Per DOD_PHASE1_INSPECTION §2.2, events are matched by their sequence number (seq).
use crate::types::{OperationEvent, Receipt};
use serde::{Deserialize, Serialize};
/// A summary entry for an event in a diff.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiffEntry {
/// The monotonic sequence number of the event.
pub seq: u64,
/// The kind of operation.
pub event_type: String,
/// First 12 hex characters of the payload commitment.
pub commitment_prefix: String,
}
impl From<&OperationEvent> for DiffEntry {
fn from(ev: &OperationEvent) -> Self {
DiffEntry {
seq: ev.seq,
event_type: ev.event_type.clone(),
commitment_prefix: ev.payload_commitment.as_hex().chars().take(12).collect(),
}
}
}
/// A detailed record of a modified event.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModifiedEntry {
/// The sequence number of the event that was modified.
pub seq: u64,
/// The event entry as it existed in the original receipt.
pub old: DiffEntry,
/// The event entry as it exists in the new receipt.
pub new: DiffEntry,
}
/// The result of a diff operation between two receipts.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct DiffResult {
/// Events that appear in the new receipt but were not in the old one.
pub added: Vec<DiffEntry>,
/// Events that were in the old receipt but are missing from the new one.
pub removed: Vec<DiffEntry>,
/// Events present in both receipts but with differing fields.
pub modified: Vec<ModifiedEntry>,
}
impl DiffResult {
/// Returns true if there are no differences between the receipts.
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
}
}
/// Compute the difference between two receipts based on event sequence numbers.
///
/// Per DOD_PHASE1_INSPECTION §2.2:
/// - Iterates events by seq in O(n) using sorted index.
/// - A seq present in `a` but not `b` -> `removed`
/// - A seq present in `b` but not `a` -> `added`
/// - A seq present in both but with different `event_type` OR different `payload_commitment` -> `modified`
pub fn diff_receipts(old: &Receipt, new: &Receipt) -> DiffResult {
use std::collections::BTreeMap;
let mut old_map = BTreeMap::new();
for ev in &old.events {
old_map.insert(ev.seq, ev);
}
let mut new_map = BTreeMap::new();
for ev in &new.events {
new_map.insert(ev.seq, ev);
}
let mut added = Vec::new();
let mut removed = Vec::new();
let mut modified = Vec::new();
// Find removed and modified
for (&seq, &old_ev) in &old_map {
match new_map.get(&seq) {
Some(&new_ev) => {
if old_ev.event_type != new_ev.event_type
|| old_ev.payload_commitment != new_ev.payload_commitment
{
modified.push(ModifiedEntry {
seq,
old: DiffEntry::from(old_ev),
new: DiffEntry::from(new_ev),
});
}
}
None => {
removed.push(DiffEntry::from(old_ev));
}
}
}
// Find added
for (&seq, &new_ev) in &new_map {
if !old_map.contains_key(&seq) {
added.push(DiffEntry::from(new_ev));
}
}
DiffResult {
added,
removed,
modified,
}
}
/// Parse two JSON strings into Receipts and compute their difference.
pub fn diff_json_receipts(old_json: &str, new_json: &str) -> anyhow::Result<DiffResult> {
let old: Receipt = serde_json::from_str(old_json)
.map_err(|e| anyhow::anyhow!("Failed to parse old receipt: {e}"))?;
let new: Receipt = serde_json::from_str(new_json)
.map_err(|e| anyhow::anyhow!("Failed to parse new receipt: {e}"))?;
Ok(diff_receipts(&old, &new))
}