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
//! Logic for building a graph representation of a receipt and exporting it
//! to Graphviz DOT or JSON formats.
//!
//! Per DOD_PHASE1_INSPECTION §3.2, the graph is a Directly-Follows Graph (DFG)
//! where nodes represent distinct event types and edges represent transitions.
use crate::types::Receipt;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// A node in the receipt graph, representing a distinct event type.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNode {
/// Unique identifier for the node (the event type).
pub id: String,
/// Human-readable label for the node.
pub label: String,
/// Number of times this event type appears in the receipt.
pub event_count: usize,
}
/// A directed edge in the receipt graph, representing a transition between event types.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
/// ID of the source node.
pub from: String,
/// ID of the target node.
pub to: String,
/// Number of times this transition occurs in the receipt.
pub weight: usize,
}
/// A graph representation of a receipt's process flow.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptGraph {
/// The nodes in the graph.
pub nodes: Vec<GraphNode>,
/// The edges in the graph.
pub edges: Vec<GraphEdge>,
}
/// Build a graph from a receipt where nodes are event types and edges are transitions.
///
/// This implements a Directly-Follows Graph (DFG) summary as required by
/// ARDPRD and the Phase 1 inspection criteria.
///
/// This implementation is iterative and safe for massive receipts (10k+ events).
pub fn build_graph(receipt: &Receipt) -> ReceiptGraph {
let mut nodes: BTreeMap<String, GraphNode> = BTreeMap::new();
let mut edges: BTreeMap<(String, String), GraphEdge> = BTreeMap::new();
// 1. Build nodes from distinct event types
for event in &receipt.events {
let node = nodes
.entry(event.event_type.clone())
.or_insert_with(|| GraphNode {
id: event.event_type.clone(),
label: event.event_type.clone(),
event_count: 0,
});
node.event_count += 1;
}
// 2. Build edges from consecutive event pairs
for i in 0..receipt.events.len().saturating_sub(1) {
let from_type = receipt.events[i].event_type.clone();
let to_type = receipt.events[i + 1].event_type.clone();
let edge = edges
.entry((from_type.clone(), to_type.clone()))
.or_insert_with(|| GraphEdge {
from: from_type,
to: to_type,
weight: 0,
});
edge.weight += 1;
}
ReceiptGraph {
nodes: nodes.into_values().collect(),
edges: edges.into_values().collect(),
}
}
/// Helper to escape DOT string literals.
///
/// Escapes `"` as `\"` and `\` as `\\` to ensure DOT validity.
fn dot_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('\"', "\\\"")
}
/// Export the graph as DOT (Graphviz) format.
///
/// The output follows the specific formatting rules defined in DOD_PHASE1_INSPECTION.md.
/// Iterative implementation ensures no stack overflow on massive graphs.
pub fn to_dot(graph: &ReceiptGraph) -> String {
let mut dot = String::with_capacity(graph.nodes.len() * 100 + graph.edges.len() * 100);
dot.push_str("digraph receipt {\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(" node [fontname=\"Courier\"];\n");
for node in &graph.nodes {
let escaped_id = dot_escape(&node.id);
let escaped_label = dot_escape(&node.label);
dot.push_str(&format!(
" \"{}\" [label=\"{} ({})\"];\n",
escaped_id, escaped_label, node.event_count
));
}
for edge in &graph.edges {
let escaped_from = dot_escape(&edge.from);
let escaped_to = dot_escape(&edge.to);
dot.push_str(&format!(
" \"{}\" -> \"{}\" [label=\"{}\"];\n",
escaped_from, escaped_to, edge.weight
));
}
dot.push_str("}\n");
dot
}
/// Export the graph as JSON format.
pub fn to_json(graph: &ReceiptGraph) -> anyhow::Result<String> {
Ok(serde_json::to_string_pretty(graph)?)
}